Skip to content

Supabase resilience runbook: probes, TTL levers, edge cache, warm standby

Build the four layers of client-side resilience against Supabase platform incidents: detection (probes that see real outages), exposure reduction (token TTL), read survival (edge cache), and full HA (warm standby with cutover). The concepts and the evidence for every number are in the companion reference, Supabase incidents: what a client can actually do.

Prerequisites. A Supabase Management API personal access token, a project on any paid plan (spend cap behavior differs on Pro), bun or Node, wrangler for the edge layer, pg_dump/psql for cold DR. The standby layer needs a second project (any region, at the compute size you will cut over to - see Part 4) - everything else uses the one you have.

Measured facts every later step depends on (supabase-lab, 2026-08-15/16):

FactValue
PostgREST iat/exp skew tolerance~30s (+30s passes, +31s rejects PGRST303)
jwt_exp config acceptance to issuer effect~6.5s
TPA resolution time~60-122ms
JWKS trust lag after TPA registration~30s cold, ~300ms warm
Standby initial sync (small table)~3.1-6.5s
Standby initial sync (1M rows)22.7s / 12.5s (two runs); lag ~245-276ms after
Standby replication lag (cross-region)34ms-1057ms
auth./storage. replicationzero changes at any tested size - do not plan on it
DDL on the primary while subscribedstalls all table replication; standby-side DDL resumes in ~6.1s
Edge failover trip condition5xx, or the CF-wrapped 403 of an unroutable origin - never >=500 alone
Edge function wall clock150s idle -> 504 IDLE_TIMEOUT
Edge function cold start~1.4s first-after-idle only; then 121-302ms vs 98ms warm
Edge function CPU or memory exhaustion546 WORKER_RESOURCE_LIMIT, identical body for both (3s busy loop; 400 MB allocation)
Edge function deploy ceilings8 MB refused 413 request entity too large via API, lands via local CLI bundling; parallel deploys can return 201 and not land - one ceiling at a time
statement_timeout signatureverbatim 57014, ~3.5s wall (2s setting vs 5s sleep)
lock_timeout signatureverbatim 55P03, ~4.5s wall (3s setting)
pg_cron across a restartresumes on schedule (2 -> 5 heartbeat rows), no doubling
Cold DR floor (10k rows via pooler)dump 12.4s, restore 6.4s
Gateway behavior on every responsesets Set-Cookie: __cf_bm
ClientEdge Workercache-first + cutover routingPrimary projectnormalStandby project(logical replication)cutoverWAL (direct host)Synthetic probes(per-service, authed)

Part 1: Detection - probes that see real outages

Section titled “Part 1: Detection - probes that see real outages”

Anonymous network probes see nothing: ACTIVE_HEALTHY means the HTTP tier is up, not that PostgREST can reach Postgres, and TCP accept is not readiness. Every probe is authenticated and hits a real table.

  1. Create a canary table on each project you monitor:
Terminal window
curl -s -X POST \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
"https://api.supabase.com/v1/projects/$REF/database/query" \
-d '{"query":"create table if not exists public.probe_canary(id int primary key); insert into public.probe_canary values (1) on conflict do nothing;"}'
  1. Probe per service, and record the PGRST code, not just the status:
Terminal window
# Data API - 200 with rows = healthy; capture body code on failure
curl -s -w '\n%{http_code}' \
-H "apikey: $PUBLISHABLE_KEY" -H "Authorization: Bearer $PUBLISHABLE_KEY" \
"https://$REF.supabase.co/rest/v1/probe_canary?select=id"

The other three services need their own probes, each against a real operation rather than a health endpoint:

  • Auth: admin-create a probe user and take the password grant (the Part 2 commands). Neither lab run probed that path: platform-downtime D01 (2026-08-04, 500 ms sampling) sampled GET /auth/v1/health with an anon key and saw HTTP 521 for 75 s across a restart; compute-disk D09 (2026-08-19, 250 ms) sampled the same /auth/v1/health and saw 0 s of contiguous outage across four resizes. Different projects, dates and vantages, so the health endpoint’s own behaviour is open; probing an authenticated operation is a design choice, not a result.
  • Storage: fetch a real object. On a fresh project Storage lags ACTIVE_HEALTHY, answering TenantNotFound and then 429 SlowDown for the first minutes (edge-resilience W21, 2026-08-17) - retry rather than page.
  • Pooler: select 1 over 6543 with a connect timeout. The error string names the operation underway (rows below). The lab’s 5 s probe timeout coarsened the pooler’s recovery resolution to about 5.5 s in timeout mode; keep the timeout short if you want the window to the second.

Declare recovery only after sustained success (the lab required 5 s of passing samples), because the pooler queues before it refuses and one passing sample mid-outage reads as recovery; and void any probe row that was already failing before the operation started, or a bad credential becomes a platform outage (platform-downtime “Reproducing”).

  1. Alert on the code classes, which separate incident types cleanly:
SignalMeaningAction
PGRST303 rateJWT claim rejection (skew incident)TTL lever (Part 2), wait for platform fix
PGRST301unknown signing keykey rotation/TPA config, not an outage
PGRST002schema-cache wedgeselect pg_notify('pgrst','reload schema')
PGRST000 / 5xx on a real tableDB or tier failureincident response
520/521/522 from CF frontgateway pathretry with backoff; cache serves reads
402billing restriction (Fair Use Policy: quota, spend cap, overdue invoice)fix billing - card, cap toggle, or usage - not the platform; details in the DR tiers reference
pooler Failed to connect to database: {:error, :timeout}project restart underway (158 s measured, platform-downtime D01)wait; REST kept serving
pooler Failed to connect to database: {:error, :econnrefused}compute resize up underway (207 s measured, D03)wait; REST kept serving
pooler terminating connection due to administrator commandcompute resize down underway (196 s measured, D04)wait; REST kept serving
pooler (EADDRNOTALLOWED) address not in tenant allow_lista network restriction was just applied and excludes this client (bites in 1 s, D02)add the client’s egress address to the allow-list

The non-engineering half of detection. Subscribe to status.supabase.com so a platform incident reaches you before your probes finish arguing about it. Know what your plan buys on the support path: Pro gets access to the support team; Team and Enterprise carry support SLAs (Urgent: 24h 24/7x365 on Team, 1h 24/7x365 on Enterprise; business-hours limits start at High - the full severity table is on the SLA page). And on Team or Enterprise, give support at least 2 weeks’ notice before a launch or heavy load event via the support form - the documented channel for getting eyes on your project during the window you are most likely to need them.

Part 2: Exposure reduction - the jwt_exp lever

Section titled “Part 2: Exposure reduction - the jwt_exp lever”

A JWT-skew incident only rejects tokens minted during the skew window. Tokens minted before it keep working, so a longer access-token TTL shrinks the cohort washed through refresh during the window.

Terminal window
# Read current value (default 3600)
curl -s -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$REF/config/auth" | jq '.jwt_exp'
# Raise to 12h (issuer-effective in ~6.5s, not instantly)
curl -s -X PATCH -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
"https://api.supabase.com/v1/projects/$REF/config/auth" \
-d '{"jwt_exp": 43200}'

Poll the readback after the PATCH, and again after the restore: the apply is async, and a readback issued immediately after a restore PATCH returned the pre-restore value (edge-resilience W17, 2026-08-17 battery; the module now polls). Then verify the effect, not the status code - issue a token and decode exp - iat. Use the admin path, because hosted /auth/v1/signup hits email rate limits under scripted load:

Terminal window
curl -s -X POST -H "apikey: $SECRET_KEY" -H "Authorization: Bearer $SECRET_KEY" \
-H "Content-Type: application/json" \
"https://$REF.supabase.co/auth/v1/admin/users" \
-d '{"email":"probe@example.com","password":"probe-pass-1","email_confirm":true}'
curl -s -X POST -H "apikey: $PUBLISHABLE_KEY" -H "Content-Type: application/json" \
"https://$REF.supabase.co/auth/v1/token?grant_type=password" \
-d '{"email":"probe@example.com","password":"probe-pass-1"}' | jq -r .access_token

Trade-off: longer-lived tokens are longer-lived if leaked. 12h is a reasonable incident posture for apps whose enterprise clients already hold day-long sessions.

Part 3: Read survival - the edge Worker cache

Section titled “Part 3: Read survival - the edge Worker cache”

An edge Worker with a cache makes an origin outage invisible for warm reads. Measured: 200 with a byte-identical body while the origin was hard-down. ORIGIN-FIRST (fetch the origin, serve cache only on failure) keeps reads fresh and still absorbs the outage - the right default for a Data API serving live rows. CACHE-FIRST (hit short-circuits before the origin) serves stale reads for up to max-age in NORMAL operation - right only for mostly-static paths. The example below is origin-first; the lab’s drill worker is the cache-first posture, because its job is outage survival, not freshness.

export default {
async fetch(req: Request, env: { UPSTREAM: string }, ctx: ExecutionContext) {
const url = new URL(req.url);
if (req.method !== "GET") return fetch(new Request(env.UPSTREAM + url.pathname + url.search, req));
const cache = caches.default;
const key = new Request(url.toString(), { headers: req.headers });
// PostgREST treats unknown query params as column filters and 400s.
// Strip probe-only params (this example: anything starting with "_")
// from the origin URL; keep the full URL only in the cache key.
const originUrl = new URL(url);
for (const k of [...originUrl.searchParams.keys()]) {
if (k.startsWith("_")) originUrl.searchParams.delete(k);
}
// Origin-first: fresh reads in normal operation, cache as the outage
// fallback. (For a mostly-static path, check cache.match(key) first
// and short-circuit on a hit - that is the cache-first posture.)
let origin: Response;
try {
origin = await fetch(new Request(env.UPSTREAM + originUrl.pathname + originUrl.search, req));
} catch {
const stale = await cache.match(key);
return stale ?? new Response('{"error":"origin unreachable"}', { status: 503 });
}
// CF Workers wraps TCP failures to unroutable origins as a 403
// RESPONSE, not a throw - include 403 here if an unroutable origin
// is your failure mode (it is the drill's).
if (origin.status >= 500) {
const stale = await cache.match(key);
if (stale) return stale;
return origin;
}
if (origin.ok) {
const toCache = origin.clone();
// The gateway sets Set-Cookie (__cf_bm) on EVERY response and the
// Cache API refuses Set-Cookie responses - strip or nothing caches.
toCache.headers.delete("set-cookie");
toCache.headers.set("cache-control", "public, max-age=86400");
ctx.waitUntil(cache.put(key, toCache));
}
return origin;
},
};

A third option is stale-while-revalidate (serve the hit, refresh in the background) when you want both freshness and latency. Whichever posture: neither invalidates on write - if clients write through the same Worker, delete the cache key on mutations.

Deploy with wrangler (wrangler deploy); set UPSTREAM to https://<ref>.supabase.co. Scope the cache to read-only paths you mean to cache - do not cache auth or per-user responses unless the cache key carries the caller’s identity. The example passes req.headers into the key Request, and that is not enough on its own: the Cache API documentation lists the response headers put() respects and says nothing about request headers as key material, so fold a hash of the Authorization header into the key URL yourself (or keep the Worker on anonymous paths, which is what the lab’s W04 drill cached). Not measured in the lab: no module cached a per-user response.

Failover variant. The lab worker extends this into an active-passive failover proxy: env vars FAILOVER_PRIMARY / FAILOVER_STANDBY / HOLD_MS; on a primary failure (5xx, the CF-wrapped 403, or any non-ok in outage mode) it re-fetches from the standby and tags the response x-drill-origin: standby; after a failure it holds on standby for HOLD_MS even if the primary recovers (flap damping), with the last-failure timestamp persisted in the Cache API so the window survives redeploys. Two rules from the measured drill: failover mode skips the cache-first read (a HIT carries no origin information and would mask the failover), and the hold window must exceed your redeploy-plus-settle path (~11s observed; the drill ships 60000 after 15000 measured an expired window). Full code: worker.ts; the drill that drives it end to end: w24-edge-failover-proxy.ts.

Part 4: Full HA - warm standby with session-portable cutover

Section titled “Part 4: Full HA - warm standby with session-portable cutover”

Measured end to end: replication lag 34ms-1057ms cross-region, sessions portable via third-party-auth, cutover rehearsable.

Run the standby at the compute size you will need at cutover, ahead of time. A resize during the incident adds its own window to your RTO: 61-105 s to settle across four resizes, the Small -> Large step settling in 61 s with 17.0 s of contiguous REST outage (compute-disk D09, 2026-08-19, 250 ms sampling), and 131 s on Auth / 207 s on the pooler for a Micro -> Small step measured at 500 ms on another rig (platform-downtime D03, 2026-08-04). If you must resize twice, space the PATCHes by at least 2 minutes; the second inside that window answered 429 We are still processing addon changes, please try again in 1-2 minutes (D09).

On the primary (single statements - see gotchas):

create publication ha_pub for table public.<your_tables>;

Before the subscription, check the slot budget on both sides: show max_replication_slots returned 10 on Micro and 10 on Small (compute-disk D01), each subscription pins one on the publisher, and a dropped subscription can leave its slot on the publisher either way - after a plain drop (W05) and after slot_name = none then drop (W14) - so list pg_replication_slots on the publisher and pg_drop_replication_slot any orphan first.

On the standby, same table DDL first (DDL does not replicate), then the subscription against the primary’s direct host - never the pooler:

create subscription ha_sub connection
'host=db.<PRIMARY_REF>.supabase.co port=5432 dbname=postgres
user=postgres password=<DB_PASSWORD> sslmode=require connect_timeout=15'
publication ha_pub;

Verify: seed rows appear on the standby (initial sync ~3.1-6.5s on a small table; 22.7s/12.5s measured for 1M rows), then insert a canary and time its appearance (34ms-1057ms cross-region; ~245-276ms right after a large initial sync). Public and custom schemas replicate; auth.* and storage.* never do - with copy_data=true the initial sync hangs in pg_subscription_rel state d (sync worker stuck at IPC/BgworkerStartup), with copy_data=false the WAL sender connects but received_lsn stays NULL, and instance size is not the variable (max_worker_processes is 6 on both micro and small).

Register the primary’s issuer on the standby once, at setup time (not at incident time):

Terminal window
curl -s -X POST -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
"https://api.supabase.com/v1/projects/$STANDBY_REF/config/auth/third-party-auth" \
-d "{\"oidc_issuer_url\": \"https://$PRIMARY_REF.supabase.co/auth/v1\"}"

Primary-issued tokens then validate against the standby’s API (measured 200). Note the cold path: the first time the standby’s PostgREST sees the issuer’s key it answers PGRST301 for ~30s. Rehearse before you need it. This keeps existing sessions alive on reads. Fresh logins on the standby hit the standby’s own auth store, and auth.* replication between managed projects is a measured dead end (zero changes stream at any tested size, micro or small, with or without copy_data), so the posture is: reads and existing sessions survive; new logins need a user backfill or a forced re-login after cutover. Two backfill paths: POST /auth/v1/admin/users accepts the source’s bcrypt password_hash and the user keeps their password (measured, tenant-consolidation C03), or copy hashes via direct SQL into auth.users (not measured on a standby); otherwise accept the re-login. Per-project auth config (SMTP, SITE_URL, redirect URLs, rate limits, jwt_exp) does not follow a cutover either - re-apply it to the standby via the Management API as part of the procedure below (measured: the diff is readable verbatim from GET /v1/projects/{ref}/config/auth).

The enterprise escape hatch: if your identity source is an external IdP via third-party auth or SAML SSO, both projects can trust the same issuer, and fresh logins work on both sides. The backfill/re-login caveat above applies to Supabase-managed password users - for accounts already federated it drops off the critical path entirely.

  1. Confirm replication lag is near zero (canary insert appears < 2s; measured ~245-276ms immediately after a 1M-row initial sync).
  2. Freeze writes on the primary (fail closed; never dual-write).
  3. Resync sequences on the standby (sequences do not replicate - the first insert without this step fails verbatim: duplicate key value violates unique constraint "<table>_pkey", measured):
select setval(pg_get_serial_sequence('public.<t>','id'),
coalesce((select max(id) from public.<t>), 1));
  1. Flip routing (DNS, or your edge Worker’s routing table) to the standby.
  2. After stabilisation, drop the old subscription and the orphaned replication slot on the old primary:
-- standby
drop subscription if exists ha_sub;
-- old primary
select pg_drop_replication_slot('ha_sub');
  1. Re-apply per-project auth config on the standby: GET /v1/projects/{ref}/config/auth on both projects, diff the entire payload, PATCH the standby. SMTP, SITE_URL, redirect URLs, rate limits and jwt_exp are the fields known to matter, and the list is not closed: custom_oauth_max_providers defaulted to 32767 on one project generation and 3 on another (W17 baseline diff, 2026-08-17). Poll the readback after the PATCH (the apply is async - Part 2).
  2. Reverse the replication direction to fail back (not measured - no module runs the reverse direction).

Note the restore interaction: a PITR or daily-backup restore of the primary unwinds this topology - subscriptions and replication slots must be dropped before a restore and recreated after (only the Realtime slot is exempted), so the standby’s subscription dies with a primary restore and must be rebuilt with a re-sync. Mid-incident, restore-to-a-new- project keeps the original (and the replication) up instead of taking the size-dependent restore downtime. When restoring into a new project, wait for ACTIVE_HEALTHY and then retry the first write: 5 of 5 fresh projects refused the first POST /auth/v1/admin/users with 500 and accepted the next attempt one poll tick later (placement reference, Verified row ‘Create -> healthy, and healthy is not writable’, 2026-08-04, n=5, bash run not in the lab repo; the lab’s own note is 2 of 2 fresh projects failing with 500 "Database error checking email" on 2026-08-03, supabase-lab AGENTS.md provisioning note - both summarised in what a platform operation costs). The full backup/PITR picture is in the DR tiers reference.

Run the cutover monthly against a canary workspace. The measured numbers say the mechanics cost seconds; the cold-path and forgotten-step costs are what rehearsals find.

On paid plans you already have daily backups (Pro 7 days, Team 14, Enterprise 30) and PITR as an add-on - the tiers, costs and restore caveats are in the DR tiers reference. The cron’d pg_dump below is the floor for FREE-tier projects, and for data too big to replicate or too cold to matter hourly:

Terminal window
# The dump runs over the pooler session host - session mode on 5432 is a
# legitimate dump path (and the W06 measurement above went through it);
# the direct host db.<ref>.supabase.co works too.
PGPASSWORD=$DB_PASSWORD pg_dump -h aws-0-<region>.pooler.supabase.com -p 5432 \
-U postgres.$REF -d postgres -Fc -f backup-$(date +%F).dump

Measured floor: 12.4s dump / 6.4s restore for 10k rows. Schedule it; ship the file to object storage outside the incident blast radius.

The practices the measured steps above add up to, grouped by part. Each names the module or RUNLOG line it rests on; module ids resolve in the edge-resilience RUNLOG, the platform-downtime RUNLOG and the compute-disk RUNLOG. A line marked “design choice” is not a result.

Detection (Part 1)

  • Probe Auth through a real operation (admin-create plus password grant) rather than /auth/v1/health alone - a design choice: both lab runs sampled /auth/v1/health, down 75 s on a restart (platform-downtime D01) and 0 s across four resizes on another rig (compute-disk D09). No module measured an authenticated Auth path.
  • Probe Storage against a real object, and on a fresh project expect TenantNotFound then 429 SlowDown for the first minutes (W21).
  • Probe the pooler with select 1 over 6543 under a short connect timeout, and map the error string to the operation using the four rows in the Part 1 table (platform-downtime pooler-mode table). With the lab’s 5 s probe timeout a timing-out pooler was resampled about every 5.5 s, which is the resolution its 158 s figure carries.
  • Declare recovery after sustained success and void rows that were failing before the operation began (platform-downtime “Reproducing”).

Exposure reduction (Part 2)

  • Poll the readback after every config/auth PATCH, including the restore; an immediate readback returned the pre-restore value (W17, 2026-08-17). The 12h value is a design choice; the ~6.5s lever is the measurement (W03).

Read survival (Part 3)

  • Put the caller’s identity into the cache key yourself before caching any per-user response; the lab cached anonymous reads only (W04), so this is a design choice, not a result.
  • The origin-first posture and max-age=86400 are design choices; the lab drilled cache-first (W04).

Warm standby (Part 4)

  • Run the standby at the cutover compute size ahead of time: a resize costs 61-105 s to settle across four resizes, the Small -> Large step settling in 61 s with 17.0 s of contiguous REST outage (compute-disk D09), or 131 s Auth / 207 s pooler on the other rig (platform-downtime D03). Space adjacent resize PATCHes by at least 2 minutes or the second answers 429 still processing addon changes (D09).
  • Check max_replication_slots (10 on Micro and Small, compute-disk D01) and drop orphan slots before CREATE SUBSCRIPTION (W05, W14).
  • Never drop and recreate a table under an active Realtime subscription (events for that name stop; new OID, stale channel metadata), and retry joins with backoff - a join after 11 other modules had the socket closed before the event (W12).
  • Diff the entire config/auth payload at cutover, not five named fields; custom_oauth_max_providers read 32767 on one project generation and 3 on another (W17).
  • When restoring into a new project, wait for ACTIVE_HEALTHY and retry the first write: 5 of 5 fresh projects refused it with 500 (placement reference, Verified row ‘Create -> healthy, and healthy is not writable’, 2026-08-04, n=5, bash run not in the lab repo; supabase-lab AGENTS.md provisioning note, 2026-08-03, 2 of 2, 500 "Database error checking email").
  • Not measured: failing back by reversing replication (4.3 step 7); a primary-side network restriction against the standby’s walreceiver; the full routing-flip RTO end to end (per-step timings only: W05, W16, W17); the monthly rehearsal cadence (design choice).
  • Probes: kill nothing, but confirm each probe reports the right code when you hand it a bad token (PGRST301) and a future-iat token (PGRST303, mintable with the lab harness).
  • TTL lever: issue a token after the PATCH, decode exp - iat = new value.
  • Edge cache: prime a URL, block the origin (point UPSTREAM at a dead host), confirm 200 from cache for the warm URL and failure for a cold one.
  • Standby: run 4.3 end-to-end on a canary workspace and time it.
  • Failover: point the Worker’s primary at an unroutable address, confirm the response flips to the standby origin tag, restore, and confirm the holdover window keeps serving standby until it expires (the lab’s W24 drill automates exactly this, four redeploys included).
  • CREATE SUBSCRIPTION must be a single-statement query. The Management query endpoint wraps multi-statement strings in one transaction, and Postgres rejects CREATE SUBSCRIPTION inside one. This presented as an unexplained SQL 400 until the verbatim body was read.
  • Config APIs are not request-path truth. TPA shows resolved while PostgREST still rejects the key (~30s cold); jwt_exp reads back before the issuer honours it (~6.5s), and a readback taken immediately after a restore PATCH still showed the old value (W17, 2026-08-17); a jwt_secret PATCH returns 200 and changes nothing. Verify at the request path, always.
  • The gateway sets Set-Cookie on everything. Strip it before cache.put or the Cache API refuses silently.
  • Cloudflare Workers wraps TCP failures to unroutable IPs as a 403 response, not a JS exception - stale-fallback catch blocks never see it. Branch on status too. For failover logic this means the trip condition must include 403, or an unroutable primary never fails over (measured 2026-08-16).
  • Cache-first and failover do not compose. A cache HIT carries no origin information, so a failover mode behind a cache-first read gets masked by warm entries. If the worker fails over between origins, skip the cache while failing over and tag every response with the origin that served it (x-drill-origin in the lab worker). A holdover window (hold on standby for N ms after a failure) damps flapping; persist the last-failure timestamp in the Cache API if it must survive redeploys.
  • PostgREST 400s on unknown query params (treated as column filters). A ?_bust= cache-buster forwarded to the origin 400s every request - strip probe-only params from the origin URL, keep them only in the cache key (measured 2026-08-16; the Part 3 example does this).
  • Standby REST reads need the standby’s own publishable key, not the primary’s. They look interchangeable and are not.
  • Wedged-subscription recovery is ordered: alter subscription <s> disable -> alter subscription <s> set (slot_name = none) -> drop subscription <s> -> pg_drop_replication_slot('<s>') on the publisher. The publisher slot survived the drop either way - after a plain drop (W05) and after slot_name = none then drop (W14) - so the final pg_drop_replication_slot step is the one that frees the WAL; subscriptions can also vanish and reappear under platform management (measured during the W09/W14 drills).
  • Break-glass secrets stay live until rotated. Reading jwt_secret through the Management API is auditable, but the secret does not expire on its own - rotate it after any break-glass use.
  • Hosted signup is not a scriptable probe surface - the default email sender rate limits (over_email_send_rate_limit). Admin-create plus password grant is the probe-safe path.
FileRole
experiments/edge-resilience/worker/worker.tsReference Worker (cache + outage drill + failover mode)
experiments/edge-resilience/tests/w05-standby-replication.tsThe standby drill, runnable
experiments/edge-resilience/tests/w24-edge-failover-proxy.tsThe failover + flap-damping drill, runnable
experiments/edge-resilience/FAILURE-MATRIX.mdThe full failure-point inventory
experiments/edge-resilience/RUNLOG.mdEvery measured number, per module
RELIABILITY.md (repo root)The incident-class reference this guide implements