Supabase incidents: what a client can actually do - a resilience reference
The platform fails in a small number of recurring ways. Each class below has a mechanism, a client-visible signature, and a workaround tier - and each claim is marked as measured in the lab (with the number) or documented-only. The implementation of every workaround here is the companion guide, the resilience runbook; the RPO/RTO/monthly-cost axis for the durability tiers is the third doc, the DR tiers reference.
Provenance. Measured numbers come from the
supabase-lab pvlab harness -
experiments/edge-resilience (2026-08-15/16, W21/W25/W26 2026-08-17) and
experiments/platform-downtime (2026-08-04, n=1 per operation, one Micro
project in ap-southeast-1); the cutover drill ran cross-region
ap-southeast-2 -> ap-southeast-1.
TL;DR - Durability has its own tier ladder, priced out in
the DR tiers reference: daily backups
(included, up to ~24h RPO), PITR (~2-minute worst-case RPO,
~$100-$400/month), warm standby (34ms-1s). Reads are cheap to
protect (edge cache, measured: byte-identical through a forced origin
outage). Writes and auth need a warm standby, which
works managed-to-managed with logical replication and sessions that survive
cutover via third-party auth (TPA, Supabase’s mechanism for trusting an external issuer’s tokens) registration (measured: 34ms-1s replication
lag, TPA resolution ~60-122ms) - with one hard boundary: platform-managed
schemas (auth.*, storage.*) do not replicate at any tested size, so
standby auth posture is token portability plus forced re-login, not user
replication. The JWT clock-skew incident class has no runtime workaround -
only exposure reduction (a token TTL lever that takes effect in ~6.5s) and
detection (the PGRST code, not the HTTP status).
Which do I pick
Section titled “Which do I pick”| You need to survive… | Tier | Cost | Evidence |
|---|---|---|---|
| Origin 5xx on read paths | Edge cache Worker | One Worker, no infra change | Measured, W04 |
| JWT claim rejections (PGRST303) | TTL raise + code-aware alerting | Config change | Measured, W01/W03 |
| Auth outage with active sessions | Longer TTL (sessions keep validating) | Config change | Measured, W03 |
| The whole PostgREST/Kong API layer (PGRST303, routing rollouts) | Own auth + claims over the wire | Rewrites the client; loses SDK surface | Measured, rls-wire-claims C01-C03 (reference) |
| Full regional/project loss | Warm standby + cutover | Second project + rehearsal | Measured, W05 |
| Anything else | Cold DR (pg_dump) | Cron + object storage | Measured, W06 |
| Data loss after the fact | PITR (point-in-time restore) | ~$100+/mo + Small compute | Documented (tiers) |
| Bad DELETE / bad migration | PITR - the standby replicates the mistake in 34ms | ~$100+/mo | Documented |
| Org-wide 402 restriction | Fix billing (card / cap / usage) | - | Documented |
| Fleet-wide incident | Contract (SLA) | Enterprise plan | Documented |
Which classes apply to you
Section titled “Which classes apply to you”Thirteen classes is a lot; most readers need four. Start from your shape:
| Your shape | Reads | Writes / auth | Durability | Classes that apply |
|---|---|---|---|---|
| Free, one project | edge cache (7) | accept the outage | scheduled db dump - Free has no backups | 1, 2, 4, 5, 7, 12 |
| Pro, one project, production | edge cache (7) | break-glass mint (W07) + TPA prep | daily backups (7d) or PITR | 1-8, 12 |
| Team / Enterprise, production | edge cache + failover (7) | warm standby (9) | PITR + standby (tiers) | all |
| Enterprise, federated IdP | same | standby; fresh logins work via the shared issuer | PITR + standby | all; the auth.* caveat drops out |
The rest of the page is the class catalog; the classes not in your row are lab-interesting until your shape changes. One caveat on the table’s plan axis: classes 3, 8, 10 and 11 are USAGE-gated, not plan-gated - they apply on any plan the moment you use the pooler, supabase-js, Edge Functions, or a query long enough to hit a timeout.
Class 1: JWT claim rejection (PGRST303)
Section titled “Class 1: JWT claim rejection (PGRST303)”Mechanism. PostgREST validates iat/exp on every request with a
30-second clock-skew tolerance.1 If the issuer’s clock runs
ahead of the validator’s, freshly minted tokens carry a future iat and
are rejected - while tokens minted before the skew began keep working. Auth
(/auth/v1/user) can return 200 while the Data API rejects the same token;
the two validate on different hosts.2
Client sees. 401 {"code":"PGRST303","message":"JWT issued at future"}.
The failure wave is every active session as its access token expires and
refreshes - the default 1-hour TTL washes the whole user base through the
broken path within one TTL.
Measured (edge-resilience W01, 2026-08-15). The documented tolerance is exact: +30s accepted, +31s rejected (401 PGRST303). Expired tokens reject with the same code; an unknown signing key rejects PGRST301. Two concurrent refreshes of the same token both return 200 (W08) - simultaneous reuse does not hard-fail.
Public incidents. 2026-08-14 “401 errors due to JWT rejections” on the Supabase status page (“newly refreshed JWTs being rejected by the API”), with an earlier “elevated JWT authorization errors” incident the same week and a matching community report three weeks prior.32 Three occurrences in a month.
Workarounds.
- Reduce exposure: raise
jwt_expvia the Management API. Readback is immediate for the new value; the issuer honours it ~6.5s later (measured, W03). The apply is async, so poll the readback after anyconfig/authPATCH including the restore - on 2026-08-17 an immediate readback returned the pre-restore value (W17). A 12h TTL means a 20-minute skew window touches a small fraction of sessions instead of all of them; the 12h figure is a choice, not a measurement. - Detect by code: alert on the PGRST303 rate. The HTTP status (401) is ambiguous; the code is not.
- Do not retry mid-skew: every refreshed token is equally future-iat. The default client already does the right thing - exactly 1 attempt on a claim rejection (measured, W02, supabase-js 2.112.3).
- No runtime fix exists. The repair is platform-side (PostgREST update plus NTP hardening). The only structural dodge is leaving the validating layer entirely: claims over the wire keeps per-user RLS while your own verifier is the only clock that matters (measured, rls-wire-claims C01/C02) - a rewrite, not a mitigation.
Class 2: Compute lifecycle downtime
Section titled “Class 2: Compute lifecycle downtime”Measured (platform-downtime D01/D03/D04, 2026-08-04, n=1 per operation,
one Micro project in ap-southeast-1, 500ms sampling). On restart: REST
and Realtime zero failed samples, Auth 75 s (HTTP 521), Storage 78 s
(HTTP 500), pooler 158 s (Failed to connect to database: {:error, :timeout}). The lab’s summary file (RELIABILITY.md) carried REST ~10s,
Storage ~26s, Auth ~75s as N=3 p50 dated 2026-08-06..08; the RUNLOG is
the record, and no artifact exists for those figures. A resize costs more
than a restart, and not by one factor: Auth 131 s against 75 s (1.75x),
pooler 207 s against 158 s (1.31x). REST and Realtime did not fail at all
under four operations at 500ms sampling - the paths do not move together.
The per-path matrix is in what a platform operation
costs.
Two probe-design lessons generalise: anonymous network probes see nothing (network accept is not readiness), and the Auth probe must be the signUp REST surface - RLS blocks the password grant on a fresh project, so a password-grant sensor reports an outage that is really its own permission model. One more measured note for runbooks: pg_cron jobs resume across a project restart and keep firing on schedule - a 1/min heartbeat job had 2 rows before the restart and 5 after (3 new rows across the window), with no catch-up doubling (measured, edge-resilience W23, 2026-08-16).
Class 3: Pooler failure modes
Section titled “Class 3: Pooler failure modes”Measured (pooler-semantics, http-tier-lockdown). Session mode: startup auth failure is a clean 58P01 at connect. Transaction mode: connect succeeds, the first statement fails 08P01/0A000, and prepared statements are unsupported. The same HTTP 400 can be a bad request or a capacity ceiling - read the SQLSTATE. Non-pooler 5432 can accept and then silently blackhole; always set connect and statement timeouts.
Class 4: Capacity and control-plane events
Section titled “Class 4: Capacity and control-plane events”Documented (public status history, 2026). Recurring multi-region project creation/resize/restart failures: April APAC (~1.3h), April us-east (~2h), June multi-region (“existing projects are not affected unless restarted or resized”), August us-east-2 project access.3 Existing projects usually keep serving.
Workaround. Pre-provision a project pool if your product provisions per-tenant, and never couple app availability to control-plane availability. There is no runtime fallback.
Your own billing state is a class here too. Under the Fair Use Policy, continually exceeding the Free quota, exceeding the Pro quota with the spend cap on, or overdue invoices restrict the whole org: projects paused, databases read-only, launches blocked, and every API request answered 402 with a restriction description in the body.4 Detection is the 402 code in the probe table; the spend cap is not a request-path breaker (W21: quota+5 renders all 200); the fix is a card, a cap toggle, or reduced usage. The full billing/cost picture is in the DR tiers reference.
Class 5: Schema-cache wedge
Section titled “Class 5: Schema-cache wedge”Measured (http-tier-lockdown). A PostgREST schema-cache wedge returns
503 PGRST002 “schema cache load”; the fix is
pg_notify('pgrst', 'reload schema'). PGRST001 on an empty exposed schema
is not a wedge. ACTIVE_HEALTHY means the HTTP tier is up, not that
PostgREST can reach Postgres - probe a real table, never the status field.
Class 6: Storage render path failure modes
Section titled “Class 6: Storage render path failure modes”Measured (edge-resilience W19, 2026-08-16). All six outcomes, verbatim:
| Source object | Render URL (?width=32) | Plain public URL |
|---|---|---|
| Valid PNG | 200, resized | 200 |
corrupt.png (text bytes) | 400 InvalidRequest - “The source image is invalid or unsupported for rendering” | 200, original bytes |
| SVG | 200, source SVG unchanged (no rasterisation) | 200 |
The render path never produced a 5xx, and the original always serves - which is exactly the degradation posture the workaround relies on.
Workarounds. Pre-generate renditions at upload (the docs-recommended architecture; the billing fix doubles as a resilience fix against the render path). The per-project transformations toggle is a hard stop with a caveat: cached transformed images may still bill after disabling, so do not promise a clean zero.
The billing half is a commercial decision, not an incident class: transformations bill per distinct origin image per billing cycle ($5 per 1,000, 100 included on Pro and Team, count resets each cycle), a growing library re-bills every month it is viewed, and the cap trade-off - cap on means eventual restriction, cap off means an uncapped bill - is priced out in the DR tiers reference.5
Class 7: Edge caching as an outage absorber
Section titled “Class 7: Edge caching as an outage absorber”Measured (edge-resilience W04, 2026-08-15). A cache-first Cloudflare Worker served warm URLs 200 with byte-identical bodies while the origin was hard-down. Cold URLs failed - only warm reads survive.
A second platform behavior matters for failure simulation: Cloudflare
Workers wraps TCP failures to unroutable addresses as a 403 response, not
a JS exception - catch-based stale fallback never fires for that failure
mode. Handle it in the status branch. The same wrap bites failover
logic: a >=500-only failover condition never trips on an unroutable
primary, because the failure arrives as a 403 response (measured,
edge-resilience W24, 2026-08-16 - the shipped worker fails over on 5xx,
403, or any non-ok in outage mode).
The failover proxy, measured end to end (edge-resilience W24, 2026-08-16). The full sequence ran clean, every step HTTP 200: prime the probe URL (origin: primary) -> redeploy with the primary pointed at an unroutable address (origin: standby) -> restore the primary and probe immediately (origin: still standby - the HOLD_MS flap-damping window, persisted in the Cache API so it survives the redeploy) -> probe after the window (origin: primary). Getting that sequence to run green surfaced three traps, each measured as a bug before it became a rule:
- Cache-first masks failover. The first iteration measured origin “none” on the prime and outage probes: the cache-first read ran before the failover logic, and HIT responses carry no origin information. A failover mode behind a cache-first read is masked by warm entries - skip the cache when failing over, and tag every response with the origin that served it.
- The 403 wrap defeats
>=500failover conditions (the wrap described above). The shipped worker fails over on 5xx, 403, or any non-ok in outage mode. - PostgREST 400s on unknown query params (treated as column
filters). The drill’s
?_w24=cache-buster, forwarded to the origin, 400’d every probe - and a 400 is not a failover condition, so the outage phase served 400s tagged primary. Strip probe-only params from the origin URL; keep them only in the cache key.
Sizing note from the same drill: the hold window must exceed the redeploy-plus-settle path between the last outage probe (which refreshes the failure timestamp) and the holdover probe - ~11s observed, so HOLD_MS=15000 measured an expired window and 60000 is what the drill ships.
Routing table isolation (W25). The multi-tenant routing variant of the drill worker isolates failures per tenant: a stale routing row pointed at a dead origin fails only that tenant (502 for it, 200 elsewhere), and the eject costs a redeploy (10.6s) while the table lives in an env var (measured, edge-resilience W25).
Class 8: Client retry behavior
Section titled “Class 8: Client retry behavior”Measured (edge-resilience W02, supabase-js 2.112.3). 401 PGRST303 ->
exactly 1 attempt (no retry amplification). 503 x3 then 200 -> 4 attempts,
success in ~7.0s. Connection refused -> surfaced in ~7.0s. The built-in
retries cover 408/409/503/504 and network failures, on by default since
v2.102.0; custom policies go through fetch-retry.6
Class 9: Warm standby and cutover (the HA tier)
Section titled “Class 9: Warm standby and cutover (the HA tier)”The auth findings in this class and in classes 1 and 8 are consolidated, with the refresh, third-party-issuer and self-hosted GoTrue measurements that postdate them, in Supabase Auth end to end.
Measured (edge-resilience W05, 2026-08-15, cross-region ap-southeast-2 -> ap-southeast-1). The full HA path works on managed projects:
- Logical replication, managed to managed: a subscription on the
standby against the primary’s direct host (
db.<ref>.supabase.co) works. Initial sync ~3.1-6.5s on a small table; replication lag 34ms-1057ms across regions. The pooler cannot be the source - it fails at the tenant-identifier layer (ENOIDENTIFIER). - Sessions survive cutover without copying secrets: register the primary’s OIDC issuer as a TPA integration on the standby (resolves in ~60-122ms) and primary-issued tokens read the standby’s API. Copying the JWT secret is not an option anyway - the config API accepts the write and changes nothing (measured 2026-08-14).
- The cold path is real: a first-time issuer’s key costs ~30s of PGRST301 before PostgREST trusts it; a previously-seen JWKS warms in ~300ms. Rehearse the cutover before you need it.
Scope note: a standby replicates your mistakes too - a bad DELETE
lands on the standby in 34ms. Logical corruption is PITR’s job, not the
standby’s.
Cutover hygiene, all measured (2026-08-16 unless noted): sequences do
not replicate - the first post-cutover insert fails with a verbatim
duplicate-key error (duplicate key value violates unique constraint "w16_t_pkey"), and setval resync restores inserts (W16). DDL does
not replicate, and primary-side DDL stalls ALL table replication -
even rows not using the new column, because the apply worker holds
streaming changes for any table not yet in r state - until the same
DDL lands on the standby, which resumes replication in ~6.1s with
backfill and no subscription recreation; apply standby DDL first
(W15). Per-project auth config (SMTP, SITE_URL, redirect URLs, rate
limits, jwt_exp) does not follow a cutover - re-apply it via the
Management API; the diff is readable verbatim from
GET /v1/projects/{ref}/config/auth on both projects (W17). Realtime
has a matching trap from the earlier round: dropping and recreating a
table under an active subscription kills events for that table NAME
(new OID, stale channel metadata) - stable canary tables only (W12).
And the wedged-subscription recovery is ordered: disable ->
set (slot_name = none) -> drop subscription ->
pg_drop_replication_slot on the publisher - dropping without the
slot detach leaves the publisher slot pinning WAL.
The replication boundary (W09/W14, measured 2026-08-15/16). The
early hypothesis was worker exhaustion on micro; sizing the standby up
disproved it. pg_settings DO scale with size (max_connections 60 -> 90,
shared_buffers 256MB -> 512MB) but max_worker_processes is 6 on both
micro and small - platform-fixed. The failure mechanics differ by
mode: with copy_data=true the initial sync stalls in
pg_subscription_rel state d forever, the sync worker hung at
IPC/BgworkerStartup with its publisher sync slot inactive; with
copy_data=false the WAL sender connects but received_lsn stays
NULL - zero changes stream either way, at any size. The discriminator:
a custom non-public schema (lab_schema.t) replicates in ~4s on the
same instances, while storage.buckets does not - and inserts via the
postgres role change nothing, so the writer role is not the filter.
The wall is specific to platform-managed schemas (auth.*,
storage.*), not size, not workers, not schema privacy. The standby
auth posture is therefore: TPA token portability for existing sessions,
user backfill through the admin API or direct SQL, or forced re-login for
fresh logins. POST /auth/v1/admin/users accepts the source’s bcrypt
password_hash and the user keeps their password (measured,
tenant-consolidation C03, 2026-08-04); copying hashes via direct SQL into
auth.users is the other path (not measured on a standby). The W09 run
(2026-08-15) posted its backfill users without a hash and recorded “not
portable via the admin API” as an assumption; an earlier version of this
paragraph repeated it, and the C03 measurement is what stands.
Sync at scale (W22). A 1,000,000-row initial sync completed in 22.7s (12.5s on a later battery run), with streaming lag ~245-276ms immediately after - the small-table numbers above extrapolate better than the stall stories suggested.
Cold DR floor (W06). pg_dump 12.4s, restore 6.4s for 10k rows through the pooler session host.
Break-glass (W07). GET /v1/projects/{ref}/postgrest returns the
project’s jwt_secret, and a locally minted token authenticates against
the live API with zero Auth involvement. During an Auth outage that is an
escape hatch; the same read grants full token minting for the project.
Prefer TPA portability - and
rotate the secret after any break-glass use: the Management API read is
auditable, but the secret stays live until rotated.
Class 10: Edge function limits
Section titled “Class 10: Edge function limits”Measured (edge-resilience W13/W18, 2026-08-15/16). The wall clock is
a 150s idle timeout - a sleeper function holding the connection past it
dies with 504 IDLE_TIMEOUT (W13). Cold start costs ~1.4s on the first
invoke after deploy plus idle; subsequent idle-cold invokes land at
121-302ms. Over 5 cold
invokes at 60s gaps: p50 284ms, p99 1433ms; warm p50 over 20 invokes is
98ms (W18). Keep-warm pings buy the difference between 98ms and ~300ms,
not the 1.4s - and long jobs belong on a queue plus cron, not in a
request path. The deploy-time ceilings (functions per project, bundle
size by deploy path, the four secrets limits) and the failures that
only look like limits (silent loss under parallel deploys, the 413 that
both size ceilings return) are measured separately in Edge Function
limits, one ceiling at a time.
Class 11: Statement and lock timeout signatures
Section titled “Class 11: Statement and lock timeout signatures”Measured (edge-resilience W20, 2026-08-16). Both surfaces return HTTP 400 - the SQLSTATE in the body is the signal, not the status:
statement_timeout(2s setting againstpg_sleep(5)): verbatimERROR 57014: canceling statement due to statement timeout, 3467ms wall.lock_timeout(3s setting on an advisory-lock contender, two sessions): verbatimERROR 55P03: canceling statement due to lock timeout, 4533ms wall.
Alert on the codes; a 400 alone is ambiguous with ordinary bad queries.
Class 12: Realtime quota events
Section titled “Class 12: Realtime quota events”Documented (2026). Realtime limits are per plan: Free 200 concurrent
connections / 100 messages per second; Pro 500 / 500; Pro without the
spend cap and Team 10,000 / 2,500; Enterprise beyond
that.7 Exceeding the message rate disconnects clients
with a tenant_events error, and supabase-js reconnects automatically
once throughput drops below the plan limit. The failure presents like an
outage - sockets dropping en masse - but it is a quota: the signature
is the disconnect reason, and the fix is plan or usage.
Class 13: What you cannot work around
Section titled “Class 13: What you cannot work around”Fleet-wide platform incidents have no client-side answer - the remedy is contractual (SLA, degradation prioritisation). The platform uptime SLA is Enterprise-only - 99.9% per product per month, measured per-project, per-region or globally depending on the product, GA features only, with service credits of 10-30% of the affected service’s monthly fees (capped at 20% of the trailing twelve months’ fees) as the sole remedy.8 Support SLAs exist for Team and Enterprise: Urgent response is 24 hours 24/7x365 on Team and 1 hour 24/7x365 on Enterprise Standard and Priority Plus; business-hours limits start at High (Team: 1 business day for High and Normal, 2 business days for Low). The full severity table is on the SLA page.8 Everyone below Team is on best-effort
- which is most of why this page exists. Read replicas are GET-only, never promoted, and Auth always goes to the Primary, so they absorb read load only.9 Multi-region active-active writes remain split-brain territory: dual-write returns 200/200 with ~107ms skew, but a partial failure (primary 200 / standby 400) is not atomic (W26) - fail over, never dual-write.
What to do about it
Section titled “What to do about it”The practices the classes above support, one row each, with the number or error it addresses and the module it rests on. Module ids resolve in the edge-resilience RUNLOG, the platform-downtime RUNLOG, the instance-sizing RUNLOG and the compute-disk RUNLOG. The platform-downtime rows are n=1 per operation, so they rest on the ordering of the paths, not on the seconds.
| Practice | Rests on |
|---|---|
| Budget a maintenance window off the resize figure: Auth 131 s and the pooler 207 s resizing up, against 75 s and 158 s for a restart. | platform-downtime D01 vs D03, 2026-08-04 |
| Keep latency-sensitive reads on PostgREST across a planned restart or resize; REST and Realtime returned zero failed samples under all four operations while the pooler was down 158-207 s. | platform-downtime full matrix, 2026-08-04 |
Read the pooler error string to identify the operation underway: {:error, :timeout} is a restart, {:error, :econnrefused} a resize up, terminating connection due to administrator command a resize down, (EADDRNOTALLOWED) address not in tenant allow_list a restriction just applied. | platform-downtime pooler-mode table |
| When adding a network restriction, allow-list your pooler clients’ egress addresses as well as your direct ones: Supavisor enforces the list against the client address, so 6543 is covered and the refusal lands 1 s after sampling starts. | platform-downtime D02, two runs |
Retry the first write after ACTIVE_HEALTHY: 5 of 5 fresh projects refused the first POST /auth/v1/admin/users with 500 unexpected_failure and accepted the next attempt one poll tick later. Expect fresh-project Storage to answer TenantNotFound and then 429 SlowDown for the first minutes. A pre-provisioned project pool (Class 4) needs both. | 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"); W21 |
Pass region_selection: {"type": "smartGroup", "code": "apac"} on POST /v1/projects when capacity matters more than the exact city: accepted on a Pro org with 201, landed ap-northeast-2, healthy in 135 s. | instance-sizing I02, 2026-08-17 |
| Retry Realtime joins with backoff: a join after 11 other modules had the socket closed before the event arrived. Never drop and recreate a table under an active subscription; events for that table name stop (new OID, stale channel metadata). | W12 |
| Keep the tenant routing table in KV or D1 rather than an env var: the env-var eject cost a redeploy at 10.6 s. The KV/D1 eject time itself is not measured. | W25, 2026-08-17; the KV/D1 half is a design choice |
Diff the whole GET /v1/projects/{ref}/config/auth payload between primary and standby rather than a named field list: 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 any config/auth PATCH, including the restore in a drill; the apply is async and an immediate readback returned the pre-restore value. | W17, 2026-08-17 battery (24/25, green on re-probe) |
Check max_replication_slots before adding a subscription (10 on micro and on small) and drop orphan slots on the publisher: each subscription pins one, 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. | compute-disk D01; W05, W14 |
Add the 520-class statuses to the client retry set through fetch-retry; the built-in set is 408/409/503/504 and network failures. Documented, not measured. | supabase-js docs6; design row |
Not measured, so not a row above: the KV/D1 eject time (W25 measured the
env-var redeploy path only); whether a primary-side network restriction
blocks a standby’s walreceiver (D02 restricted the pooler, the direct-host
subscription path was never restricted); failing back by reversing the
replication direction; the 12h jwt_exp value (the lever is measured,
the value is a choice); read replicas keeping GETs alive through a
lifecycle operation. One discrepancy is open: compute-disk D09
(2026-08-19, local vantage, 250 ms) saw 0 s of contiguous outage on
/auth/v1/health across four resizes, while platform-downtime D03
(2026-08-04, 500 ms) saw 131 s of HTTP 521 on the same /auth/v1/health
for one. Different rigs, projects and vantages, so it is a side-by-side
run to do; no module has probed an authenticated Auth operation across a
resize.
Decision guide
Section titled “Decision guide”Evidence table
Section titled “Evidence table”| Claim | Value | How it was checked |
|---|---|---|
| Skew tolerance boundary | +30s pass, +31s PGRST303 | Measured (W01, two runs) |
| jwt_exp acceptance-to-effect | ~6.5s | Measured (W03) |
| supabase-js on PGRST303 | 1 attempt | Measured (W02, mock) |
| Edge cache under origin outage | 200, byte-identical | Measured (W04, forced outage) |
| Standby replication lag | 34ms-1057ms | Measured (W05, three runs) |
| Standby initial sync, 1M rows | 22.7s / 12.5s; lag after ~245-276ms | Measured (W22, two runs) |
| auth./storage. replication | zero changes at any tested size | Measured (W09/W14) |
| DDL on primary stalls all table replication | resume ~6.1s after standby DDL | Measured (W15) |
| Post-cutover insert without resync | duplicate key; setval fixes | Measured (W16) |
| Edge function cold start | ~1.4s first-after-idle only; p50 284ms cold vs 98ms warm | Measured (W18) |
| Edge function wall clock | 150s idle -> 504 IDLE_TIMEOUT | Measured (W13) |
| Render path on invalid source | 400 InvalidRequest; original still serves | Measured (W19) |
| statement_timeout signature | 57014 verbatim, 3467ms wall | Measured (W20) |
| lock_timeout signature | 55P03 verbatim, 4533ms wall | Measured (W20) |
| pg_cron across restart | resumes on schedule (2 -> 5 rows), no doubling | Measured (W23) |
| Edge failover sequence | primary -> standby -> holdover -> primary, all 200 | Measured (W24) |
| Tenant routing isolation | poisoned row: 502 for its tenant, 200 elsewhere; eject = redeploy (10.6s) via env-var table | Measured (W25) |
| Storage dual-write | parallel 200/200, ~107ms skew, bytes equal; partial failure not atomic; sync-after 97ms | Measured (W26) |
| Spend cap enforcement | none at the request path (quota+5 renders all 200) | Measured (W21) |
| TPA resolution on standby | ~60-122ms | Measured (W05) |
| Cold kid trust on cutover | ~30s PGRST301 | Measured (W05 full suite) |
| Cold DR, 10k rows | dump 12.4s, restore 6.4s | Measured (W06) |
| Break-glass minting works | 200 real / 401 wrong secret | Measured (W07) |
| Concurrent refresh race | both 200 | Measured (W08) |
| Restart gaps REST/Realtime/Auth/Storage/pooler | 0 failed samples / 0 / 75 s / 78 s / 158 s | Measured (platform-downtime D01, 2026-08-04, n=1, 500 ms); RELIABILITY.md carried ~10s / ~26s / ~75s (N=3) with no artifact |
| Resize up Auth/Storage/pooler | 131 s / 127 s / 207 s | Measured (platform-downtime D03, 2026-08-04, n=1) |
Fresh project: first write after ACTIVE_HEALTHY | 5 of 5 refused with 500, next attempt accepted | Measured elsewhere (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"); summarised in operation cost) |
| Smart-group region selection on a Pro org | 201, landed ap-northeast-2, healthy in 135 s | Measured (instance-sizing I02, 2026-08-17) |
max_replication_slots on micro and small | 10 / 10 | Measured (compute-disk D01) |
| 30s documented tolerance | documented | PostgREST v11 docs1 |
| Transform billing model | $5/1000 distinct origins/cycle | Supabase docs5 |
All measured rows come from the
supabase-lab pvlab harness
(experiments/edge-resilience),
2026-08-15/16/17, and are reproducible from the experiment’s Makefile -
the full run log with every number is
RUNLOG.md.
Batteries: 2026-08-16, 22 modules, 22/22
(out/2026-08-16);
2026-08-17, 25 modules, 24/25 with W17 green on re-probe after its readback
race was fixed
(out/2026-08-17).
The platform-downtime, instance-sizing and compute-disk rows cite their
RUNLOGs at the same commit; those experiments have no published out/
artifact.
References
Section titled “References”-
PostgREST, “API Configuration and Custom Claims,” PostgREST Docs v11. https://docs.postgrest.org/en/v11/references/auth.html ↩ ↩2
-
Supabase GitHub Discussion #48123, “Fresh Supabase Auth JWT rejected by PostgREST: PGRST303 ‘JWT issued at future’,” 2026-07-21. https://github.com/orgs/supabase/discussions/48123 ↩ ↩2
-
Supabase, “Status - Incident History,” status.supabase.com. https://status.supabase.com/ ↩ ↩2
-
Supabase, “Billing FAQ - Fair Use Policy,” Supabase Docs. https://supabase.com/docs/guides/platform/billing-faq#fair-use-policy ↩
-
Supabase, “Manage Storage Image Transformations usage,” Supabase Docs. https://supabase.com/docs/guides/platform/manage-your-usage/storage-image-transformations ↩ ↩2
-
Supabase, “Automatic retries in supabase-js,” Supabase Docs. https://supabase.com/docs/guides/api/automatic-retries-in-supabase-js ↩ ↩2
-
Supabase, “Realtime Quotas,” Supabase Docs. https://supabase.com/docs/guides/realtime/limits ↩
-
Supabase, “Service Level Agreement.” https://supabase.com/sla ↩ ↩2
-
Supabase, “Read Replicas,” Supabase Docs. https://supabase.com/docs/guides/platform/read-replicas ↩