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.
Constants preamble
Section titled “Constants preamble”Measured facts every later step depends on (supabase-lab, 2026-08-15/16):
| Fact | Value |
|---|---|
| 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. replication | zero changes at any tested size - do not plan on it |
| DDL on the primary while subscribed | stalls all table replication; standby-side DDL resumes in ~6.1s |
| Edge failover trip condition | 5xx, or the CF-wrapped 403 of an unroutable origin - never >=500 alone |
| Edge function wall clock | 150s 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 exhaustion | 546 WORKER_RESOURCE_LIMIT, identical body for both (3s busy loop; 400 MB allocation) |
| Edge function deploy ceilings | 8 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 signature | verbatim 57014, ~3.5s wall (2s setting vs 5s sleep) |
| lock_timeout signature | verbatim 55P03, ~4.5s wall (3s setting) |
| pg_cron across a restart | resumes 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 response | sets Set-Cookie: __cf_bm |
Architecture overview
Section titled “Architecture overview”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.
- Create a canary table on each project you monitor:
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;"}'- Probe per service, and record the PGRST code, not just the status:
# Data API - 200 with rows = healthy; capture body code on failurecurl -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/healthwith an anon key and sawHTTP 521for 75 s across a restart; compute-disk D09 (2026-08-19, 250 ms) sampled the same/auth/v1/healthand 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, answeringTenantNotFoundand then429 SlowDownfor the first minutes (edge-resilience W21, 2026-08-17) - retry rather than page. - Pooler:
select 1over 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”).
- Alert on the code classes, which separate incident types cleanly:
| Signal | Meaning | Action |
|---|---|---|
| PGRST303 rate | JWT claim rejection (skew incident) | TTL lever (Part 2), wait for platform fix |
| PGRST301 | unknown signing key | key rotation/TPA config, not an outage |
| PGRST002 | schema-cache wedge | select pg_notify('pgrst','reload schema') |
| PGRST000 / 5xx on a real table | DB or tier failure | incident response |
| 520/521/522 from CF front | gateway path | retry with backoff; cache serves reads |
| 402 | billing 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 command | compute resize down underway (196 s measured, D04) | wait; REST kept serving |
pooler (EADDRNOTALLOWED) address not in tenant allow_list | a 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.
# 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:
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_tokenTrade-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).
4.1 Replicate
Section titled “4.1 Replicate”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).
4.2 Make sessions portable
Section titled “4.2 Make sessions portable”Register the primary’s issuer on the standby once, at setup time (not at incident time):
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.
4.3 Cutover procedure
Section titled “4.3 Cutover procedure”- Confirm replication lag is near zero (canary insert appears < 2s; measured ~245-276ms immediately after a 1M-row initial sync).
- Freeze writes on the primary (fail closed; never dual-write).
- 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));- Flip routing (DNS, or your edge Worker’s routing table) to the standby.
- After stabilisation, drop the old subscription and the orphaned replication slot on the old primary:
-- standbydrop subscription if exists ha_sub;-- old primaryselect pg_drop_replication_slot('ha_sub');- Re-apply per-project auth config on the standby: GET
/v1/projects/{ref}/config/authon both projects, diff the entire payload, PATCH the standby. SMTP,SITE_URL, redirect URLs, rate limits andjwt_expare the fields known to matter, and the list is not closed:custom_oauth_max_providersdefaulted 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). - 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.
4.4 Rehearse
Section titled “4.4 Rehearse”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.
Part 5: Cold DR floor
Section titled “Part 5: Cold DR floor”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:
# 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).dumpMeasured floor: 12.4s dump / 6.4s restore for 10k rows. Schedule it; ship the file to object storage outside the incident blast radius.
What to do about it
Section titled “What to do about it”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/healthalone - 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
TenantNotFoundthen429 SlowDownfor the first minutes (W21). - Probe the pooler with
select 1over 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/authPATCH, 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=86400are 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 beforeCREATE 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/authpayload at cutover, not five named fields;custom_oauth_max_providersread 32767 on one project generation and 3 on another (W17). - When restoring into a new project, wait for
ACTIVE_HEALTHYand 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).
Verification
Section titled “Verification”- 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
UPSTREAMat 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).
Gotchas and lessons learned
Section titled “Gotchas and lessons learned”CREATE SUBSCRIPTIONmust be a single-statement query. The Management query endpoint wraps multi-statement strings in one transaction, and Postgres rejectsCREATE SUBSCRIPTIONinside one. This presented as an unexplained SQL 400 until the verbatim body was read.- Config APIs are not request-path truth. TPA shows
resolvedwhile PostgREST still rejects the key (~30s cold);jwt_expreads 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); ajwt_secretPATCH returns 200 and changes nothing. Verify at the request path, always. - The gateway sets
Set-Cookieon everything. Strip it beforecache.putor the Cache API refuses silently. - Cloudflare Workers wraps TCP failures to unroutable IPs as a 403
response, not a JS exception - stale-fallback
catchblocks 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-originin 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 afterslot_name = nonethen drop (W14) - so the finalpg_drop_replication_slotstep 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_secretthrough 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.
File reference
Section titled “File reference”| File | Role |
|---|---|
experiments/edge-resilience/worker/worker.ts | Reference Worker (cache + outage drill + failover mode) |
experiments/edge-resilience/tests/w05-standby-replication.ts | The standby drill, runnable |
experiments/edge-resilience/tests/w24-edge-failover-proxy.ts | The failover + flap-damping drill, runnable |
experiments/edge-resilience/FAILURE-MATRIX.md | The full failure-point inventory |
experiments/edge-resilience/RUNLOG.md | Every measured number, per module |
RELIABILITY.md (repo root) | The incident-class reference this guide implements |