What a Supabase platform operation costs a client
A restart, a compute resize, a network restriction, provisioning a fresh project, and moving one between organizations each interrupt a client differently: on a different connection path, for a different duration, by a different failure mode. A plan that assumes “a restart takes about a minute” can be checked here, per path, against what happened.
TL;DR:
- REST reads and Realtime subscriptions went down under a restart and under a resize in either direction, for about as long as Auth and Storage: restart 247 and 194 s, Micro to Small 171 and 218 s, Medium to Large 113 s (the second run, 299 s, is void: the orchestrator slept 263 s inside it, lab #211) for the row read (2026-10-23; two passes on one Micro project, one run each on two Medium projects). The 2026-08-04 “never failed” was the HTTP tier answering.
- On 2026-08-04 a resize cost more than a restart, but not by a constant factor: Auth 131 s against 75 s is 1.75x; the pooler 207 s against 158 s is 1.31x. Those are Micro to Small. On 2026-10-23 the order did not hold (Auth restart 247 and 192 s against Micro to Small 171 and 218 s). A Medium to Large resize on another project took Auth 35 s, Storage 34 s and the pooler 30 s (medium-serverless MS07, 2026-09-30, REST and Realtime probed only at the HTTP tier); two Medium projects resized Medium to Large on 2026-10-23 took 107 to 115 s (B; C’s Medium to Large is void, the orchestrator slept inside it) on the row read, the Realtime join, Auth, Storage and the pooler, and MS07 re-run the same day on two more took every data path down 118 to 310 s across eight legs, Auth 128 to 221 s. Neither region nor project age since the create call up to one hour explains the 2026-09-30 figures.
- compute-disk D09 re-ran Micro -> Small -> Large -> Small -> Micro on
2026-10-23 (n=1 per op): REST reads, Auth and Realtime joins down 144.528
to 254.886 s per resize, REST back 18 to 35 s after
ACTIVE_HEALTHY. Its 2026-08-19 figures (1.0 s and 17.0 s REST outages) are withdrawn. - A network restriction reaches the pooler in ~1 s and leaves the HTTP tier and the data path alone (0 failed row reads and Realtime joins in two runs, 2026-10-23). The reported 62 s window is mostly a chosen 60 s test dwell, not a platform duration.
- A restart re-checked on 2026-10-10 (n=5, one Micro project) had Auth 54 s,
Storage 58 s, pooler 54 s and direct 50 s at p50, so the n=1 D01 figures of
75, 78 and 158 s are one observation of a window that was about 30 s in two
of five re-check runs and 50 to 60 s in the other three. The same module
re-run on 2026-10-23 with the data-path probes had REST reads and Realtime
joins failing in 5 of 5 restarts (p50 150 s and 146 s) and the six paths
that failed at 43 to 182 s (
rest_httpandrealtime_ws0 of 5); why the two days differ is not explained. - The restart-pooler window (158 s) fails by a 5 s timeout (
TIMEOUT_MS = 5000inprobes.ts), so it carries multi-second resolution despite the rest of the matrix sampling at 500 ms. - pg_cron jobs resume across a restart on schedule, with no catch-up doubling - a 1/min heartbeat job had 2 rows before the restart and 5 after, measured separately (supabase-lab edge-resilience W23, 2026-08-16). An instant inside the restart is skipped rather than run late, and a run in flight is recorded failed (pg_cron behaviour, CB04/CB05, 2026-10-28).
- Provisioning a fresh project to
ACTIVE_HEALTHYtook 131-159 s (n=5); moving a project into a Free organization cost 75.2 s continuous, because the instance resizes to Nano on arrival.
Method
Section titled “Method”One probe per connection path - PostgREST, Auth, Storage, Realtime, and the pooler on 6543 - each sampled independently every 500 ms while the operation was triggered out of band.
What counts as answering differs per path.
| Path | Counts as answering |
|---|---|
| PostgREST, Auth, Storage | any HTTP status other than a 5xx |
| Realtime | the WebSocket handshake completes |
| pooler (6543) | a connection opens and select 1 returns |
The HTTP rule is loose on purpose: anon receives a 401 from /rest/v1/ and a
400 from Storage without a bearer, so a probe demanding 200 would report a
permanent outage on two paths. That looseness is what made the REST column
wrong: the 401 comes from the API gateway before PostgREST or the database is
reached, so the probe answered whether the gateway was up; it never attempted
a read, and the Realtime handshake never touches the database either.
The 2026-10-23 re-measure kept both probes (renamed rest_http and
realtime_ws) and added two that read data: rest_read, an anon
GET /rest/v1/pvlab_probe?id=eq.1&select=id that counts only HTTP 200 with
exactly that row, and realtime_join, a fresh socket that counts only when the
Postgres Changes subscription is confirmed and the probe’s own broadcast comes
back1. The pooler’s rule is stricter.
EADDRNOTALLOWED under a network restriction is a reply - the pooler is
reachable and refusing - but it is scored as a failure, because the probe asks
whether a query reached Postgres.
Recovery required five seconds of sustained success rather than one passing sample. The reported recovery time is the first sample of that sustained run; the settle period confirms recovery and is not counted into the outage.
A path already failing before the operation voided that row.
The direct port 5432 was not tested: it is IPv6-only and unreachable from this vantage, so there is no unpooled baseline here.
What breaks, and for how long
Section titled “What breaks, and for how long”The REST and Realtime columns in this first table are the HTTP-tier probes
(the 401 at /rest/v1/ and the WebSocket handshake), so their dashes say only
that the API gateway and the WebSocket endpoint answered; the data path is in the
2026-10-23 table. Even so,
the paths do not move together: on 2026-10-23 the row read, the Realtime join,
Auth and Storage stayed within 8 s of each other in every run except Small to
Micro, but the pooler ran longest by 15 to 96 s in six of the ten restarts and
resizes (D02 and B’s no-op D03 excluded; C’s Medium to Large, one of the other
four, is void since lab #211), and a single duration for “a
restart” or “a resize” throws away which path failed, for how long, and by
what failure mode.
| operation | REST (HTTP tier) | Realtime (handshake) | Auth | Storage | pooler |
|---|---|---|---|---|---|
| restart | - | - | 75 s | 78 s | 158 s |
| restart, re-check 2026-10-10 (LO05, p50 of 5) | - | not sampled | 54 s | 58 s | 54 s |
| network restriction | - | - | - | - | see below |
| resize up (micro -> small) | - | - | 131 s | 127 s | 207 s |
| resize down (small -> micro) | - | - | 99 s | 100 s | 196 s |
| resize up (medium -> large) | - | - | 35 s | 34 s | 30 s |
| resize down (large -> medium) | - | - | 42 s | 44 s | 39 s |
A dash is zero failed samples. The Medium rows come from a different project
and day (medium-serverless MS07, ap-southeast-2, 2026-09-30, n=1 each
way); their pooler column is Supavisor 6543, and the same run watched four
more paths (up / down): shared 5432 36 s / 39 s, dedicated PgBouncer 6543 34 s / 55 s,
direct 5432 35 s / 41 s, and the HTTP-tier REST probe and the Realtime
handshake with zero failed samples both ways (MS07 had no row read then; its
2026-10-23 re-run with one is below). The dedicated pooler was the last path back on the way down,
answering server login has been failing, cached error: connect failed (server_login_retry) for about 14 s after Postgres was up. The restriction row has no duration because its
window is a test parameter; the
network restriction section
has the dwell and what was measured around it.
The restart row above is n=1 (D01, 2026-08-04). The re-check row is a separate
run on 2026-10-10 (LO05): one Micro project in ap-southeast-1, restarted five
times in sequence, each after a readiness check, from one macOS orchestrator
with IPv6, with REST, Auth, Storage, the pooler on 6543 and the direct port
5432 sampled every 0.5 s. Realtime was not sampled, hence its cell. The figures
are the p50 of the five windows; the direct path, which D01 could not reach, had
a p50 of 50 s.
| path | runs with a failure | window p50 | window max |
|---|---|---|---|
| REST (HTTP tier) | 0 of 5 | - | - |
| Auth | 5 of 5 | 54 s | 60 s |
| Storage | 5 of 5 | 58 s | 59 s |
| pooler (6543) | 5 of 5 | 54 s | 151 s |
| direct (5432) | 5 of 5 | 50 s | 56 s |
- The first failed sample came 2-3 s after the
POST /restarton every path that failed (direct: p50 3 s, max 3 s), which matches the 2 s bite time of D01.POST /restartanswered 200 in about a tenth of a second each time. - The control plane read
ACTIVE_HEALTHYagain at 40, 41, 61, 61 and 71 s on a 10 s poll, ahead of the data plane in at least one run: in run 2 the status readACTIVE_HEALTHYat 41 s while the pooler window ran to 151 s. A script that waits forACTIVE_HEALTHYcan hand traffic to a pooler that has not recovered. - The windows split into two groups. Runs 1 and 2 were about 30 s on every path except the pooler’s 151 s in run 2; runs 3 to 5 were 50 to 60 s on every path (Auth 32, 31, 54, 59, 60 s by run; pooler 31, 151, 52, 54, 59 s). Five sequential restarts of one project cannot separate drift across back-to-back restarts from platform load or chance.
- The 2026-08-04 pooler figure of 158 s is close to this run’s single 151 s and not to its other four. Different day, different project, and Realtime dropped: not a controlled comparison, and the re-check does not replace the D01 row.
- A second
POST /restartsent while the status readRESTARTINGreturned 200 with an empty body, as the first had (LO05c). Whether it queued a second restart or was absorbed into the first was not sampled.
How these windows read from a client, and the workarounds, is in the incident resilience reference; provisioning and fleet operations through the same API are in the platform Management API guide.
On the same restart that left Auth unavailable for 75 s, the HTTP-tier REST
probe and the Realtime handshake answered continuously at 500 ms resolution,
which says the gateway and the WebSocket endpoint stayed up and nothing about
reads. Auth and Storage also fail differently from each other, HTTP 521
against HTTP 500.
Re-measured with data-path probes (2026-10-23)
Section titled “Re-measured with data-path probes (2026-10-23)”The same four modules (D01 to D04) ran again on 2026-10-23 with rest_read
and realtime_join beside the older probes, on throwaway projects in a Pro
organization in ap-southeast-1: project A (Micro, two passes), and projects
B and C (created at Medium, one Medium to Large and back each). A and B, then
A and C, ran at the same time from one machine. Sampling was every 500 ms, but
a failing request often takes its full timeout (5 s for the HTTP probes, 10 s
for realtime_join), so a path that fails by timeout is resampled about every
5.5 s or 10.5 s and its recovery end carries up to that much slack. Outage
window in seconds, pass 1 / pass 2 on A, B / C on Medium2:
| operation | REST read | Realtime join | Auth | Storage | pooler |
|---|---|---|---|---|---|
| restart (A) | 247 / 194 | 242 / 191 | 247 / 192 | 250 / 195 | 329 / 216 |
| network restriction (A) | 0 failed samples, both passes | 0 failed samples, both passes | 0 failed samples, both passes | 0 failed samples, both passes | 60 / 61, inside the 60 s dwell |
| Micro to Small (A) | 171 / 218 | 168 / 214 | 171 / 218 | 174 / 218 | 189 / 216 |
| Small to Micro (A) | not sampled / 54 | not sampled / 67 | 191 / 54 | 196 / 51 | 249 / 150 |
| Medium to Large (B / C) | 113 / void | 107 / void | 115 / void | 113 / void | 113 / void |
| Large to Medium (B / C) | 217 / at least 101 | 217 / at least 98 | 216 / at least 101 | 217 / at least 104 | 217 / at least 200 |
- C’s Medium to Large is void and its Large to Medium figures are lower bounds: the orchestrator machine slept 263 s inside the first, with every path up on the first sample after it, and 43 s just before the second’s first failure, so that outage began unobserved (lab #211). The first published version of this table carried 298 to 299 s for C’s Medium to Large.
rest_readfailed withHTTP 521(andHTTP 525in B’s Large to Medium);realtime_joinwithpg_changes: {:error, "Too many database timeouts"}or an:rpc_error. The channel join itself was acknowledged; the Postgres Changes subscription is what failed, which a handshake probe cannot see.- Through the same windows the HTTP-tier probes had at most one failed sample per run, a single request that timed out.
- Small to Micro on pass 1 has no data-path figures: its 180 s readiness budget ran out while the test machine slept 263 s (lab #211), so that pass ran with the HTTP-tier probes only, and the data path for that direction is n=1.
- These windows are well above the 2026-08-04 restart (75 to 78 s for Auth and Storage) and the 2026-10-10 re-check p50s (54 to 58 s). Different day and projects; not explained.
The same day, LO05 re-ran on one Micro project with the data-path probes, five
sequential restarts3. REST reads and Realtime joins failed in 5
of 5, p50 150 s and 146 s, max 181 s and 179 s, with the first failure 2 s
after the restart at p50; Auth (p50 149 s), Storage (151 s), the pooler
(147 s) and direct (145 s) failed alongside. rest_http and realtime_ws
failed in 0 of 5. Per run, rest_read was down 181, 154, 46, 46 and 150 s:
runs 3 and 4 look like the 2026-10-10 windows and the other three do not.
MS07 (medium-serverless) re-ran Medium to Large and back the same morning with
the same data-path probes on two throwaway Medium projects in a Pro
organization, E in ap-southeast-2 (the 2026-09-30 region) and F in
ap-southeast-1, about 4 minutes and one hour after each create call, n=1 per
leg4. Outage window in seconds, up (Medium to Large) / down:
| path | E, 4 min | E, 1 h | F, 4 min | F, 1 h | 2026-09-30 |
|---|---|---|---|---|---|
| REST read | 222 / 183 | 140 / 195 | 179 / 190 | 127 / 211 | not probed |
| Realtime join | 212 / 184 | 138 / 200 | 180 / 193 | 118 / 213 | not probed |
| Auth | 221 / 183 | 141 / 195 | 180 / 190 | 128 / 211 | 35 / 42 |
| Storage | 222 / 184 | 140 / 195 | 179 / 190 | 124 / 212 | 34 / 44 |
| shared 6543 | 222 / 180 | 141 / 194 | 173 / 238 | 125 / 310 | 30 / 39 |
| dedicated 6543 | 235 / 197 | 140 / 209 | 193 / 204 | 128 / 210 | 34 / 55 |
| direct 5432 | 223 / 182 | 139 / 194 | 177 / 189 | 128 / 210 | 35 / 41 |
- Every data path went down in all eight legs, for 118 to 310 s; the REST
read was within 1 s of Auth in each leg, and the HTTP-tier
rest_httpandrealtime_wshad 0 failed samples throughout. The 2026-09-30 “REST and Realtime untouched” was the HTTP-tier probe. - Region does not explain the 2026-09-30 figures (E ran in that region and
measured Auth 141 to 221 s), and neither does age since the create call up
to one hour. Untested differences between the two days: org plan (Team then,
Pro now), Postgres image (
17.6.1.166then,17.11.0.003now), the 2026-09-30 project’s two hours of earlier modules, and the platform itself. - F’s shared pooler ran longest on the way down (238 and 310 s against 189 to
213 s on F’s other paths), its errors running on to
(ECIRCUITBREAKER) too many authentication failuresafter Postgres was back.
compute-disk D09 re-ran its four resizes on 2026-10-23 on one Pro project in
ap-southeast-1 with a row read, /auth/v1/health with the anon key and a
Realtime join, sampled every 250 ms, the Realtime join every 1 s, n=1 per op5. The longest REST
read outage was 231.465 s on Micro -> Small, 223.28 s on Small -> Large,
152.466 s on Large -> Small and 254.886 s on Small -> Micro, Auth and the
Realtime join within 10 s of it in each; the status read ACTIVE_HEALTHY 218,
216, 136 and 245 s after the PATCH and REST reads recovered 18 to 35 s after
that. The 2026-08-19 D09 figures that stood in other pages (REST outages of
1.0 and 17.0 s, settle 61-105 s) are withdrawn: the old module dropped
outages still open at a phase end and timed PATCHes refused with 429 as
resizes.
Time to bite is not one number either
Section titled “Time to bite is not one number either”Bite time is measured from when sampling started, not from when the
operation’s own API call returned: sampleDuring in sampler.ts sets t0
before the probe loops begin, and the operation runs inside that same sampled
window, so the reported time already includes however long that call itself
took to complete. It also differs by operation.
| operation | bite time, HTTP tier | bite time, pooler |
|---|---|---|
| restart | 2 s | 2 s |
| network restriction | - | 1 s |
| resize up | 2 s | 3 s |
| resize down | 2 s | 3 s |
None of it is a flat 2-3 s range, and none of it is measured against the API’s response.
The explanation this suggests, which was not tested
Section titled “The explanation this suggests, which was not tested”The 2026-08-04 reading was that the four HTTP services insulate a client from a restart because they reach Postgres from inside the project. The data-path probes withdraw that for restarts and resizes: the API gateway and the Realtime WebSocket endpoint kept answering, and PostgREST reads, Realtime joins, Auth and Storage all failed while Postgres was down. What is left is narrower. The services reach Postgres from inside the platform, so a network restriction on the database leaves them serving reads (0 failed row reads and Realtime joins in two runs) while it refuses the pooler, whose client socket is subject to the allow-list.
That is an inference from the result, not something measured here; the topology was not inspected.
The same topology as a list:
- The client reaches four HTTP services - PostgREST, Auth, Storage and Realtime - and, on the pooled path, Supavisor on 6543.
- The API gateway and the Realtime WebSocket endpoint answered throughout; on 2026-10-23 PostgREST reads, Realtime joins, Auth and Storage all failed while Postgres was down; the pooler and Postgres were the slowest paths in the 2026-08-04 runs.
- The four HTTP services, and the pooler, each reach Postgres.
- A direct connection to Postgres on 5432 was not tested here: it is IPv6-only from this vantage.
Resize against restart: no fixed order
Section titled “Resize against restart: no fixed order”On 2026-08-04 Auth was down 75 s restarting and 131 s resizing up, 1.75x. The pooler was down 158 s restarting and 207 s resizing up, 1.31x. The two ratios are far enough apart that neither stands in for the other. On 2026-10-23 the order did not hold: Auth was down 247 and 192 s restarting against 171 and 218 s resizing Micro to Small on the same project.
Direction matters unevenly too. On the HTTP tier, growing costs about a third more than shrinking: Auth 131 s up against 99 s down. On the pooler the two directions are closer, 207 s up against 196 s down, which is +5.6% and just outside 5%. The 2026-10-23 runs do not keep that ordering: Auth was down 171 and 218 s up against 191 and 54 s down on one Micro project, so treat the direction as unsettled.
Compute size is set by an addon mutation rather than a resize endpoint: PATCH on the project’s billing addons with an addon_type of
compute_instance6. Returning to the smallest size REMOVES the
addon rather than setting one, and the subsequent read reports no compute
addon.
The network restriction only reaches the pooler
Section titled “The network restriction only reaches the pooler”Locking the database to a CIDR that excluded this vantage left REST, Auth, Storage and Realtime serving - four paths, zero failed samples, across two runs. Those were the HTTP-tier probes; on 2026-10-23 the row read and the Postgres Changes join also had zero failed samples in two runs. The restriction controls database sockets and leaves the HTTP tier and its reads alone.
It did reach the pooler, biting 1 s after sampling started, with a refusal that named the rejected address. The restriction covers the pooled port as well as a direct connection.
The pooler error string, per operation and per day
Section titled “The pooler error string, per operation and per day”| operation | pooler error, 2026-08-04 | pooler error, 2026-10-23 |
|---|---|---|
| restart | Failed to connect to database: {:error, :timeout} | terminating connection due to administrator command (D01, both passes; LO05 also logged timeout expired and (EAUTHQUERY) authentication query failed: connection to database not available) |
| network restriction | (EADDRNOTALLOWED) address not in tenant allow_list | the same, both passes |
| resize up | Failed to connect to database: {:error, :econnrefused} | terminating connection due to administrator command (A both passes, B, C) |
| resize down | terminating connection due to administrator command | the same on A (both passes) and B; timeout expired on C |
terminating connection due to administrator command is a Postgres message;
the others come from Supavisor. On 2026-08-04 the four operations each
answered a different way: unreachable, refused, deliberately shut down, or the
client not permitted. On 2026-10-23 restarts, resizes up and three of four resizes
down all answered terminating connection due to administrator command, so only
EADDRNOTALLOWED still names its operation by itself.
Sampling resolution is not uniform either
Section titled “Sampling resolution is not uniform either”The probe timeout is 5 s (TIMEOUT_MS = 5000 in probes.ts), and each
path’s sampler loop is serial: await probe.run(); await sleep(intervalMs).
A sample that fails BY TIMEOUT can take up to that full 5 s to return, before
the 500 ms sleep between samples even starts.
Bite time is recorded against when a sample STARTS, not when it returns (see Time to bite is not one number either), so a slow-to-return timeout does not push the reported bite time later. It does delay every sample after the first failure: on a path that keeps timing out, the next attempt cannot start until the previous one has waited out the full timeout. The loop’s period becomes the 5 s timeout plus the 500 ms sleep, so a path stuck in timeout mode is resampled about every 5.5 s, and the recovery boundary is only located to within that interval.
The restart’s pooler mode is {:error, :timeout} - a timeout, not a fast
refusal. Its bite (2 s) was recorded on a sample that started while the
previous sample had still returned fast, but for the rest of the 158 s window
the pooler was resampled at roughly 5.5 s intervals rather than 500 ms. The
158 s therefore carries several seconds of slack at the recovery end.
In the 2026-08-04 runs, fast-failing modes were unaffected: econnrefused,
EADDRNOTALLOWED, HTTP 521 and HTTP 500 returned immediately rather than
waiting out the timeout, so those windows kept 500 ms resolution throughout;
on 2026-10-23 an HTTP 521 read was resampled about every 1.2 s (206 failed
samples across D01’s 247 s window on project A, pass 1).
The ~2x difference between the pooler and the HTTP tier on restart survives this: 158 s and 75-78 s are tens of seconds apart whichever side of the gap carries more slack. It does mean the numbers on this page do not share one sampling resolution.
The 2026-08-04 gap is one run. In the 2026-10-10 restart re-check (LO05, n=5) the pooler’s p50 window was 54 s against 54 s for Auth and 58 s for Storage, and its window was longer than Auth’s in 1 of the 5 runs (run 2: 151 s against 31 s, derived from the per-run windows). A pooler that is reliably about twice the HTTP tier is therefore not a figure to carry from one restart.
Every published figure is also rounded to a whole second: flatten() divides
each millisecond count by 1000 and rounds before it is reported, so a window
described as “158 s” was some value between 157.5 and 158.5 seconds in the
raw sample data.
What this rig could not measure
Section titled “What this rig could not measure”A freshly created project comes up already at the latest app version:
eligible: false, current equal to latest, no upgrade targets. The version
selectors on project creation are deprecated and typed null in the published
schema6, and the project this rig created took the current default,
so the version fields give no way to create one that has already aged past its
version.
A field typed null is not necessarily ignored. Measured 2026-10-10 on one Small
project per engine in ap-southeast-1 (OR01a, OR01b), postgres_engine is
typed deprecated null on the create body in the published OpenAPI
document6, yet POST /v1/projects with postgres_engine: "17-oriole"
was accepted and applied. The project read back 17-oriole, PostgreSQL 17.6,
aarch64, OrioleDB public beta 14, while the heap control, created without the
field, read 17.11 on x86_64. release_channel is typed the same way on that
body; whether a non-default value of it is honoured was not tested. The engine
field selects a different build line rather than an older copy of the default
one, and the upgrade eligibility of the Oriole project was not read, so the
aged-project limitation below still holds.
Measuring a client-visible upgrade window therefore needs a project that has aged past the current version, which this rig’s throwaway-project method cannot produce. That limits the rig and says nothing about how long an upgrade takes a client.
Without upgrading anything, the eligibility payload carries
duration_estimate_hours, which returned 1 on each of three aged projects
in this organization, all offering a single patch-level target with no
validation errors. That is a value the API returned, not an observed outage -
and this page has already shown paths differing by more than 2x on the same
operation. Three projects returning exactly 1 also reads like a coarse,
published estimate rather than a per-project computation.
What a fresh project costs before its first write
Section titled “What a fresh project costs before its first write”Measured 2026-08-04 against a live organization, n=5, in the multitenant reference - a separate run from the restart/resize/restriction matrix above, on the same day but a different project set.
| Operation | Measured | Method |
|---|---|---|
Create project -> ACTIVE_HEALTHY | 131-159 s (n=5, median 131 s) | poll GET /v1/projects/{ref} |
ACTIVE_HEALTHY -> first write succeeds | +1 poll tick (5 of 5 refused the first write) | poll, then retry the write |
| Delete project (API call) | ~2 s | DELETE /v1/projects/{ref} |
ACTIVE_HEALTHY was not writable on any of those five projects: each refused
its first POST /auth/v1/admin/users with 500 unexpected_failure and
accepted the next attempt, one poll tick later. A script that provisions a
project and immediately creates a user has to retry.
Two lifecycle runs on 2026-10-10 (LO03, LO05a; one Micro-requested project per
create in ap-southeast-1, one Pro organization, a different day and project
set from the table above) read differently, and the two sets are unreconciled:
| Operation | Measured 2026-10-10 | Method |
|---|---|---|
Create -> ACTIVE_HEALTHY (LO03, n=3) | at the first 10 s poll twice (2 s after the POST), at 145 s once (COMING_UP at 3 s) | 10 s poll from before the POST |
| Project that read healthy at the first poll (LO05a) | REST, Auth, Storage, pooler and direct all answered 5 s after that poll | one probe of each path |
| Create call (LO03) | 2358, 1499 and 1656 ms | time of the POST /v1/projects call |
The earlier table has 131-159 s in 5 of 5 creates and a refused first write
in 5 of 5; this run has an immediate ACTIVE_HEALTHY in 2 of 3 and, in the one
immediate case that was probed, every path answering. The lifecycle run records
probe answers, not a write, so the two do not test the same thing on the
write side, and n=3 says both a 2 s and a 145 s create happen rather than how
often. The size actually applied was not read back, and no cause for the
difference was found.
A restart sent right after a create was refused in one observation (LO05f) and accepted in another:
- About 70 s after the creating
POST, on a project handed over from the duplicate-create check below,POST /v1/projects/{ref}/restartanswered400withProject restarts are only allowed ten minutes after the creation process has completed.followed by text the artifact cut off. The 70 s is read off the timestamps of two modules, not timed by one. - 300 s after the first
ACTIVE_HEALTHYread, on a different project, a restart was accepted (200, empty body) and restarted the project. The runs that followed, from 696 s after that read, were accepted with no refusals.
The message says ten minutes and a restart at 300 s went through, so either
the stated window is not what is enforced or the clock it runs from (“the
creation process has completed”) is not the first ACTIVE_HEALTHY read. The
boundary between about 70 s and 300 s is unresolved, and the two observations
are on two projects.
Create bodies that are invalid fail fast and create nothing (LO03a, one request each, 55-194 ms):
| Request | Answer |
|---|---|
| unknown region code | 400 region_selection.code: Need to use one of available regions. |
| unknown organization slug | 404 Not Found |
missing name | 400 name: Invalid input: expected string, received undefined |
| unknown instance size | 400 desired_instance_size: Need to use one of available instance sizes. |
The organization listing held no project with the run’s name tag afterwards.
A create whose response the caller abandoned does create the project (LO04,
n=1): with the client wait capped at 1 s, the POST timed out client-side,
and the listing then held one project with that name. The identical request
re-sent 1 s later answered 400 in 0.17 s with Project with name "<name>" already exists in your organization., and 60 s later there was still one
project with that name, ACTIVE_HEALTHY. A client that retries after a lost
response therefore has to read that 400 as “probably created, look it up by
name”. A retry wrapper that adds a random suffix per attempt would create a
second project; that consequence is reasoned from the 400, not run. Not
measured: whether the name match is case-sensitive, whether a name freed by
DELETE is reusable at once, and two concurrent identical requests.
What moving a project between organizations costs
Section titled “What moving a project between organizations costs”Measured 2026-08-03 against live projects moved between a Team-plan, a
Pro-plan and a Free-plan organization, polling /auth/v1/health every 250 ms
across each move, in the org-consolidation
guide - a separate run from both tables
above.
| Direction | Measured |
|---|---|
| Paid to paid | 2.3 s, no outage above the probing host’s noise floor |
| Paid to Free | 75.2 s continuous |
A Free organization only offers Nano compute7, so the instance is
resized on
arrival; during that window the edge returns HTTP 521 and HTTP 525 rather
than a clean 503. The 75.2 s is the arrival resize, charged to the transfer;
a paid-to-paid move produces no data-plane interruption distinguishable from
measurement noise.
What a restore costs
Section titled “What a restore costs”Measured 2026-10-10 on throwaway projects in ap-southeast-1, driven over the
Management API and the shared pooler from one IPv4-only workstation (RP01 to
RP03). In the PITR runs REST, Auth and Realtime were probed every 1 s, the
pooler by a login every 5 s, and the project status every 10 s; the Free runs
polled status every 5 s for pause and every 10 s for unpause. A separate run
(RP01) read which restore routes a Pro project exposes and timed nothing, so it
has no row in any table above. The tiers, what a restore does not bring back, and the cost of
each are in the DR tiers reference; this
section adds the per-path outage of the two restores a lab organization could
drive.
Point-in-time restore on Small
Section titled “Point-in-time restore on Small”PITR needs at least Small compute8. The subject was a Pro project
on Small with the pitr_7 add-on, a 2 GB gp3 disk, a seed row, a bucket and
an object (RP02, five runs). Each run chose a target instant, rotated the
database password 15 s later, inserted a second row and called
restore-pitr; a control then rotated again, chose a target after that
rotation and restored again. That is 9 clean restores: 4 with a target before
a rotation and 5 controls.
| Restore | Back to ACTIVE_HEALTHY (10 s polling) | n |
|---|---|---|
| target before a rotation, 10 MB database | 30 s | 3 |
| target before a rotation, 574 MB database | 81 s | 1 |
| control, target after a rotation | 40, 30, 41, 40, 30 s (median 40 s, 30-41 s) | 5 |
Two of the five controls ran on larger databases (715 MB at 41 s, 574 MB at 40 s), so the 81 s at 574 MB is a single point.
| Path | Failed samples across the 9 clean restores |
|---|---|
| REST (HTTP tier), Auth, Realtime (handshake) (1 s probes) | none |
| Storage | a 12 s failure window in 8 of 9 restores, 24 s in one; The operation timed out, never an HTTP 5xx |
| pooler (5 s login poll) | 5-13 s on the 10 MB runs and on a 715 MB control; 41 s and 23 s on the 574 MB runs |
REST, Auth and Realtime recorded one simultaneous timeout on all three, 338 s
after the first restore request of the 574 MB run, with no recovery recorded
and no explanation. RP02 had no Data API read: its REST probe is the same
anon GET /rest/v1/ the gateway answers with 401. Later restores that carried
one (rest_read, a service_role read up only on 200) saw it answer
503 PGRST002 for 15 and 14 s in two restores of a database a few minutes old
(RP06 R24 and R25, 30 s and 41 s to healthy, 2026-10-23) and for 192 to 544 s
in seven restores of 3.2 to 10.5 GB databases (RP07, 2026-10-22 and
2026-10-23; the test machine slept during these runs and the lab has not yet checked which samples that removed, lab #211), while the HTTP-tier REST probe (rest) in the same runs failed
in 1 of those 9 restores, for 6 s (R18, 3 GB); Auth failed for 144 s in one
3 GB RP07 restore (R19) and the Realtime handshake for 144 s (R18) and 6 s
(R19)9. The pooler errors were the database system is not accepting connections, is starting up, is shutting down, econnrefused and
terminating connection due to administrator command. The 41 s pooler window
sat on the 574 MB database and the 5-13 s one on a 715 MB database, so the
database size does not order them.
- The first
restore-pitrcall was refused in 8 of the 9 clean restores with400Recovery time target must be within range: <earliest> <= t <= <latest>; where the target was recorded it sat 19-96 s past the quoted upper bound. A retry 60 s later (or 121 s later) was accepted. Retries ran in 60 s steps, so the lag is bounded and not resolved. - With
pitr_7just applied, the first backups read already hadpitr_enabled=trueand a window of 359 or 360 s in 5 of 5 runs. - After every clean restore the current password logged in through the pooler and the replaced one was refused. The current password was refused for one 5 s poll between “the database answers” and “the password works” in 6 of 7 restores observed at that resolution: a short stale window of unmeasured length. The changelog says current credentials are reapplied after every physical restore10, and the stored verifier differed from both the pre-rotation and post-rotation value each time, which is what that predicts (an inference from fingerprints).
- Storage listed, downloaded (bytes equal) and accepted an upload with
200after every restore. A restore to before an object’s upload made its download404and the listing omit it, and writing the same path again answered200. - Every target was minutes old. A target hours or days old, a database over
1 GB and direct 5432 were not run. Seeding a 1500 MB database on the 2 GB
disk filled it at 715 MB, and the first restore request after that was
refused as
Database appears to be unreachableand accepted at 301 s; what filled the disk was not separated. - A poller that tried a known-wrong password every 4 s plus a 1 s probe opened
the pooler’s circuit breaker within about a minute, and every login, the
correct password included, then answered
(ECIRCUITBREAKER) too many authentication failures, new connections are temporarily blockedbefore the password was checked. The poller moved to once a minute for the replaced password, and the breaker reappeared only in the state left by the full disk above.
Free pause and unpause
Section titled “Free pause and unpause”A Pro organization has no pause (POST /pause answers 400 Project is not free-tier, entitlement project_pausing false on the Pro organization, RP01),
so the only pause and unpause a lab organization could run was a Free project
(RP03, 3 runs of 2 cycles = 6 cycles, one project per run).
| Measurement | 5 of 6 cycles | The outlier cycle |
|---|---|---|
POST /pause -> INACTIVE (5 s polling) | 62, 62, 62, 63, 62 s | 276 s |
unpause -> ACTIVE_HEALTHY (10 s polling) | 183-212 s | 444 s |
| REST answers | 15-20 s | 6 s |
| Realtime answers | 16-18 s | 445 s |
| Auth answers | 152-175 s | 445 s |
| Storage answers | 181-217 s | 445 s |
| pooler login with the current password | 178-211 s | 445 s |
The status passed through INACTIVE -> COMING_UP -> RESTORING -> ACTIVE_HEALTHY on each unpause. The outlier, the first cycle of the third run, is slow on both
operations and no cause was found; other runs were using the same control
plane at the time. Its pause-time sampling window ended before INACTIVE, so
its pause-path columns are partial. “Answers” on an HTTP path is any non-5xx,
so REST at 15-20 s means the gateway answered, not that PostgREST served rows.
- While a project was pausing, the first failing sample read
HTTP 540on REST and Auth, and on StorageHTTP 500in 2 of the 3 runs, and the pooler answeredtenant/user ... not found. - The password set before the pause worked after the unpause (3 of 3 first
cycles) and the original was refused. A password change while the project was
INACTIVEwas refused (400 Cannot reset password for non-active projects), so the stale-credential case the changelog describes10 could not be set up through that route. - Storage list, download and upload answered
200after every unpause.
Restore to a new project (a clone) was not run. The published OpenAPI document
has no clone path and no source field on the create body, and the Dashboard’s
clone route answers a PAT 401 Unsupported access token. POST database/backups/restore and GET database/backups/restore-point answered
400 This endpoint is unavailable at the moment on a fresh Pro project, and
the same by a manual call on a project with pitr_7 applied. Read those as
not run.
What a password change and a config change cost
Section titled “What a password change and a config change cost”Measured 2026-10-01 on one Micro project in ap-southeast-1
(platform-downtime D05), a separate run from the tables above: one
PATCH /v1/projects/{ref}/database/password to a throwaway value, then
PostgREST, Auth, Storage and Realtime sampled every 500 ms for a 420 s
window, with the password restored afterwards. pg_postmaster_start_time()
read 2026-10-01T06:38:25.763Z before and after the window, and none of the
four paths recorded a failed sample. On that project the password change did
not restart Postgres within the 420 s window. The pooler was not probed: its
probe authenticates with the password being changed, so a failure there could
not be told apart from a stale credential. n=1, on a project provisioned with
OpenTofu through the Management API; a project whose database credentials an
integration syncs was not tested. The PostgREST and Realtime paths were HTTP-tier probes.
Re-run on 2026-10-23 with the data-path probes (D05, one throwaway Micro
project in a Pro organization, ap-southeast-1, two passes)11:
pg_postmaster_start_time() read 2026-10-23T07:44:20.792Z before and after
both passes, and the REST read and the Realtime join had 0 failed samples (pass
1: rest_read 0 of 767, realtime_join 0 of 662 over 420 s; pass 2: 0 of 154
and 0 of 132 over about 83 s, because the sampler stops once a single
realtime_ws failure at +77 s had recovered). The password change did not
restart Postgres and did not touch REST reads or Realtime joins. The pooler
stayed out of the probe set for the same reason as before.
A Postgres config change through the CLI is documented rather than measured
here. The custom-config docs say CLI v2 “checks the parameter’s context and
requests the correct action (reload or restart)”: a reloadable setting
(pg_settings.context = 'sighup') is applied with a reload, and parameters
marked “Restart: Yes” restart the database and any read replicas12.
--no-restart defers that restart; the docs warn that leaving a changed
parameter pending can break a read replica if the primary or the replica
later restarts on its own. max_wal_size and work_mem are listed
“Restart: No”. Whether a “Restart: No” change ever restarts the database was
not measured.
Whether a platform restart keeps Postgres’s cumulative statistics
(pg_stat_checkpointer, pg_stat_statements) was not measured here.
Locally, how a stop ended and the Postgres major version decided it for
pg_stat_checkpointer (pg_stat_statements was not measured); those
measurements are in
the Grafana monitoring guide.
Reading the numbers
Section titled “Reading the numbers”n=1 per operation on 2026-08-04, except the restriction and the restart re-checks. One run each for restart (D01) and both resize directions, two runs for the network restriction, all on projects in a single organization; the 2026-10-10 restart re-check (LO05) is five restarts of one other project; on 2026-10-23 each cell is n=2 at most and LO05 again five restarts. The ordering repeated across every operation and project in the 2026-08-04 matrix; the HTTP-tier REST probe answering repeated in the 2026-10-10 re-check (5 of 5), but the row read failed in 5 of 5 on 2026-10-23; the pooler being the slowest path did not repeat (1 of 5 runs on 2026-10-10). The individual seconds are not worth much confidence either: the same restart read 75 s on Auth in D01 and 54 s at p50 in LO05.
500 ms resolution, except where the pooler times out. The HTTP tier and the non-timeout pooler modes were sampled every 500 ms. The restart-pooler window fails by a 5 s timeout and was resampled at that timeout’s cadence for most of its duration, not at 500 ms - see Sampling resolution is not uniform either.
One vantage, IPv4, one account. The direct port was never in the matrix,
so the pooler column has nothing unpooled to compare against, and every
restart/resize/restriction figure here is from a single organization in
ap-southeast-1 observed from Singapore.
Provisioning and transfer are separate sessions. The 131-159 s provisioning window and the 75.2 s paid-to-Free transfer window were measured on different days, by different runs, against different projects than the restart/resize/restriction matrix above - each section carries its own date and method.
The result supports that these paths, on these operations, did not move together on this account, in this region, on these dates. It does not support any figure for a different region, plan, project size, or point in time.
The rate budget your polling runs against
Section titled “The rate budget your polling runs against”Every method in this doc polls the Management API to watch an operation, so the API’s own throttle is part of the rig’s envelope. Measured 2026-08-17 on a normal paid organization:
- every response carries
x-ratelimit-limit,x-ratelimit-remaining, andx-ratelimit-reset, so the budget is readable off the wire. The limit read 120 and the remaining count decremented 1:1 per call on a scoped single-project read. The budget is cumulative across a user’s PATs: alternating two tokens from the same user, each token’s remaining count drops on the other token’s calls - one shared counter per user per scope, so PAT sharding does not multiply it (measured 2026-08-18). - a deliberate burst on that cheap scoped read tripped
429 {"message":"ThrottlerException: Too Many Requests"}at request 118, withretry-after: 60and a clean 200 after the window. On this endpoint the 429 body is the API’s own JSON. Other endpoints and burst shapes have produced an HTML page from the edge layer instead, so a client still treats “non-JSON body” and “JSON 429” as the same back-off signal.
At 500 ms sampling the rig spends 120 requests per minute per poller -
exactly the limit the response headers report - which is why the pollers here read
x-ratelimit-remaining rather than assuming the ceiling, and why a
platform polling a fleet has to budget per-scope, not per-process.
What to do about it
Section titled “What to do about it”Every row from the 2026-08-04 restart/resize/restriction matrix is n=1 (the 2026-10-10 and 2026-10-23 restart re-checks are n=5, the 2026-10-23 matrix n=2 at most per cell), so it rests on the ordering of the paths rather than on the seconds; the rows from other runs carry their own n. Module ids resolve in the platform-downtime RUNLOG, the lifecycle-ops RUNLOG, the restore-paths RUNLOG, the orioledb RUNLOG, the compute-disk RUNLOG and the instance-sizing RUNLOG.
| Practice | Evidence | Module |
|---|---|---|
| Budget a Medium to Large resize at up to 6 minutes on every path, reads included. | MS07 (2026-09-30), up: Auth 35 s, Storage 34 s, shared 6543 30 s, shared 5432 36 s, dedicated 6543 34 s, direct 35 s; down: 42 / 44 / 39 / 39 / 55 / 41 s; its REST and Realtime 0 failed samples both ways were HTTP-tier probes. On 2026-10-23 two other Medium projects lost REST reads for 113 s up (B; C’s up leg is void) and 217 s and at least 101 s down, and the MS07 re-run on two more took every data path down 118 to 310 s across eight legs (Auth 128 to 221 s), so the 2026-09-30 figures are the outlier, unexplained by region or age since create. Give pooled clients a retry budget past the dedicated pooler’s cached login failure: it answered server login has been failing, cached error: connect failed (server_login_retry) for about 14 s after Postgres was back in MS07. n=1 per project each way. | medium-serverless MS07 (2026-10-23); platform-downtime D03, D04, 2026-10-23 |
| Size a maintenance window off the longer of the restart and resize figures. | Auth 131 s and the pooler 207 s resizing up, against 75 s and 158 s restarting (2026-08-04); on 2026-10-23 Auth was down 247 and 192 s restarting against 171 and 218 s resizing Micro to Small. | D03 vs D01, 2026-08-04 |
| Do not count on PostgREST or Realtime to ride out a restart or resize. | Row reads and Postgres Changes joins failed for about as long as Auth and Storage: restart 247 and 194 s, Micro to Small 171 and 218 s, Medium to Large 113 s for the read (C void) (2026-10-23). The 2026-08-04 “zero failed samples” came from an anon GET /rest/v1/ the gateway answers with 401 and a WebSocket handshake. | platform-downtime D01, D03, D04, 2026-10-23 |
| Give clients on every path a retry budget of about 5 minutes across a planned resize. | The pooler was down 207 s resizing up and 196 s resizing down (2026-08-04). On 2026-10-23 the longest path per resize ran 115 to 249 s (one Large to Medium pooler figure, 200 s, a lower bound), and REST reads failed alongside, so failing reads over to REST does not cover the window. | D03, D04; 2026-10-23 re-run |
Read (EADDRNOTALLOWED) from the pooler as a restriction; the other strings do not name the operation. | On 2026-08-04 {:error, :timeout} was 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; on 2026-10-23 restarts and resizes up both answered terminating connection due to administrator command, and one resize down timeout expired. | pooler-mode table |
| When applying a network restriction, include your pooler clients’ egress addresses. | Supavisor enforces the allow-list against the client address, so 6543 is covered and the refusal bites 1 s after sampling starts. | D02, two runs |
| Space adjacent compute-resize PATCHes by at least 2 minutes. | The second inside the window answered 429 We are still processing addon changes, please try again in 1-2 minutes. On 2026-10-23 none of three follow-on PATCHes was refused when each went out after every data path had been up for 15 s (D09, n=3 PATCHes). | compute-disk D09, 2026-08-19 |
When a script returns a project to Micro, poll the addon GET for null, not for ci_micro. | Micro is the absence of a compute addon, and returning to it removes the addon. | D04 |
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 it 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); supabase-lab AGENTS.md provisioning note (2026-08-03, 2 of 2, 500 "Database error checking email") |
Budget about 75 s of HTTP 521 / HTTP 525 before moving a production project into a Free organization, and accept Nano compute afterwards. | The exact window is in the transfer table above; it is the arrival-side resize running as part of the transfer, not the transfer itself. | transfer table above (2026-08-03 probe in the org-consolidation guide; no lab RUNLOG) |
| Do not wake a legacy free-era paused project on a paid org unless you accept it can never be re-paused. | The restore was accepted, ACTIVE_HEALTHY arrived about 30 minutes later, and POST /v1/projects/{ref}/pause then answered 400 {"message":"Project is not free-tier. Please downgrade it to free-tier first and try again."}. Pause eligibility follows the org’s current plan, and a Pro organization has no pause at all: on 2026-10-10 a fresh Pro project answered the same 400 Project is not free-tier to POST /pause, with entitlement project_pausing false on the Pro organization (consistent with RP03, where pause and unpause could only be run on a Free project). | instance-sizing I03, 2026-08-18; restore-paths RP01, RP03, 2026-10-10 |
Budget 120 requests per minute per user per scope and read x-ratelimit-remaining off the wire. | Two PATs from one user share one counter, so PAT sharding does not multiply the budget. Treat a non-JSON 429 body and the JSON ThrottlerException as the same back-off signal. | rate-limits L01a (1:1 decrement on the scoped read), L01c (429 at request 118 with retry-after: 60 and a JSON ThrottlerException body), L01b (two PATs from one user share one counter), 2026-08-17/18; L02 (JSON throttler with retry-after 59-60 in all 15 burst cells, 2xx again after 65 s), 2026-10-13 |
| Check a parameter’s “Restart” column before changing it with the CLI. | Per the docs, CLI v2 applies a reloadable (sighup) setting with a reload and restarts the database and any read replicas for a “Restart: Yes” parameter; --no-restart defers that restart and can leave a read replica out of step. Confirm the value took with SHOW <parameter>;. Documented, not measured. | none (docs12) |
| Expect no restart from a database-password PATCH on a project like D05’s (one Micro project, no credential-syncing integration). | pg_postmaster_start_time() unchanged across a 420 s window and zero failed samples on the HTTP-tier REST probe, Auth, Storage and the Realtime handshake (2026-10-01, n=1); re-run 2026-10-23 with a row read and a Realtime join, two passes on one Micro project: postmaster_start unchanged, 0 failed rest_read and realtime_join samples. The pooler was not probed. | platform-downtime D05, 2026-10-01; D05 2026-10-23 |
Wait on the pooler path, not ACTIVE_HEALTHY, before sending traffic back after a restart. | The status read ACTIVE_HEALTHY again at 40-71 s on a 10 s poll while the data paths had failed at 2-3 s; in run 2 it read healthy at 41 s with the pooler window running to 151 s. n=5, one Micro project. | lifecycle-ops LO05, 2026-10-10 |
| Do not send a restart to a project minutes old; read the 400 as “wait”. | A restart about 70 s after the create was refused 400 Project restarts are only allowed ten minutes after the creation process has completed; one 300 s after the first ACTIVE_HEALTHY read was accepted on another project. Boundary unresolved. | lifecycle-ops LO05f, 2026-10-10 |
| After a create whose response was lost, list by name before retrying. | An abandoned create made the project; the identical re-send answered 400 Project with name "<name>" already exists in your organization. n=1. | lifecycle-ops LO04, 2026-10-10 |
| Budget 30-81 s for a small PITR restore on Small, with Data API reads and Storage down for part of it. | ACTIVE_HEALTHY in 30 s (10 MB, n=3), median 40 s for a target after a rotation (30-41 s, n=5), 81 s at 574 MB (n=1); the HTTP-tier REST probe, Auth and the Realtime handshake no failed 1 s sample, Storage a 12 s window in 8 of 9, pooler 5-13 s and 41 s at 574 MB. A Data API read in two later small restores answered 503 PGRST002 for 15 and 14 s. | restore-paths RP02, 2026-10-10; RP06 R24, R25, 2026-10-23 (RUNLOG) |
| Budget about 3.5 minutes for a Free unpause and retry Auth, Storage and the pooler past it. | Unpause to ACTIVE_HEALTHY 183-212 s in 5 of 6 cycles (444 s once), the HTTP-tier REST probe answering at 6-20 s, the Realtime handshake 16-18 s (445 s once), no row read, Auth 152-175 s, Storage 181-217 s, pooler 178-211 s. | restore-paths RP03, 2026-10-10 |
| Poll a wrong password at most once a minute against the pooler. | A known-wrong password every 4 s opened the circuit breaker within about a minute, and every login then answered (ECIRCUITBREAKER) before the password was checked. | restore-paths RP02, aborted first run |
Not measured, so not a row: whether the restriction leaving reads alone is a
topology property (inferred); direct 5432 during the 2026-08-04 operations
(IPv6-only from that vantage); any upgrade window (duration_estimate_hours: 1
is an API value); a restart on any size above Micro, a resize above Large, or a
vantage other than Singapore. Read replicas kept GETs answering, stale,
while the primary restarted, but not while the replica did: the restart took
both nodes down one after the other and the load balancer sent none of the
failed reads to the primary (replica-routing RR02i, 2026-10-23, two runs;
read replicas).
Evidence
Section titled “Evidence”| Claim | How it was checked |
|---|---|
| Restart: REST (HTTP-tier probe) and Realtime (handshake) zero failed samples; Auth 75 s, Storage 78 s, pooler 158 s | Measured, 500 ms sampling except the pooler mode (a timeout, resampled at the 5 s timeout’s cadence), one Micro project, 2026-08-04 |
Restart re-check: REST (HTTP-tier probe) never failed; Auth 54 s, Storage 58 s, pooler 54 s, direct 50 s p50 (maxes 60, 59, 151, 56 s); first failure 2-3 s; ACTIVE_HEALTHY at 40-71 s | Measured, LO05, 0.5 s sampling, n=5 sequential restarts of one Micro project, ap-southeast-1, 2026-10-10; Realtime not sampled |
Second POST /restart while RESTARTING returned 200 | Measured, LO05c, one observation, 2026-10-10; whether it queued was not sampled |
| Resize up 131/127/207 s, down 99/100/196 s | Measured, same probe set, same project, 2026-08-04 |
| REST and Realtime “never failed” under restart and resize | Withdrawn 2026-10-23: the REST probe was an anon GET /rest/v1/ the API gateway answers with 401 before PostgREST or the database, and the Realtime probe a WebSocket handshake |
| Data path under restart and resize: REST read 247/194 s restart, 171/218 s Micro to Small, 54 s Small to Micro, 113 s Medium to Large (C void), 217 s and at least 101 s Large to Medium; Realtime join within 6 s of the read in each run except Small to Micro (67 s against 54 s) | Measured, D01-D04 with rest_read and realtime_join, 500 ms sampling, three throwaway projects (Micro A two passes, Medium B and C one run each), ap-southeast-1, 2026-10-23 |
| Data path under restart, n=5: REST read and Realtime join failed 5 of 5, p50 150 s and 146 s, max 181 s and 179 s | Measured, LO05 re-run, one Micro project, ap-southeast-1, 2026-10-23; unreconciled with the 2026-10-10 run’s p50s |
| Restriction leaves the HTTP tier serving | Measured twice, zero failed samples on four paths, 2026-08-04; the row read and the Realtime join also zero failed samples, two runs, 2026-10-23 |
| Restriction bites in ~1 s, recovers in ~2 s, inside a chosen 60 s dwell (62 s total) | Measured; the dwell was chosen, not measured |
| Per-operation pooler error strings | Captured verbatim from the failing probe |
| Compute size is an addon mutation | Read from the published OpenAPI document6 |
| A fresh project is at the latest version and cannot be created older through the version fields | Read from the eligibility endpoint and the create schema6; nothing was upgraded |
postgres_engine typed deprecated null on the create body yet applied (17-oriole, PostgreSQL 17.6, aarch64, OrioleDB public beta 14; control 17.11 x86_64) | Schema read6 against a create and read-back, OR01a and OR01b, one Small project per engine, 2026-10-10 |
duration_estimate_hours of 1 on three projects | Read from the eligibility endpoint; not an observed outage |
Provisioning: create -> ACTIVE_HEALTHY 131-159 s, delete ~2 s | Measured, n=5, 2026-08-04, against a live organization |
First admin/users write after healthy fails 500, retry succeeds | Measured, 5 of 5 projects, 2026-08-04 |
Create -> ACTIVE_HEALTHY at the first poll twice and 145 s once | Measured, LO03, 10 s poll, n=3, 2026-10-10; unreconciled with the 131-159 s above |
| A project healthy at the first poll answered on REST, Auth, Storage, pooler and direct | Measured, LO05a, one project, 5 s after the poll, 2026-10-10; no write was recorded |
| Restart 70 s after a create refused 400; restart 300 s after the first healthy read accepted | Measured, LO05f, two projects, 2026-10-10; boundary unresolved |
Invalid create bodies fail fast and create nothing; an abandoned create still creates and the re-send is 400 already exists | Measured, LO03a (4 requests) and LO04 (n=1), 2026-10-10 |
| PITR restore on Small: 30 s (10 MB, n=3), median 40 s after a rotation (n=5), 81 s at 574 MB (n=1) | Measured, RP02, 10 s status polling, 2026-10-10 |
| PITR restore per path: REST (HTTP-tier probe), Auth, Realtime (handshake) no failed sample; Storage 12 s; pooler 5-13 s and 41 s at 574 MB | Measured, RP02, 1 s probes (5 s pooler login poll), 9 clean restores, 2026-10-10; no Data API read |
PITR restore, Data API read: 503 PGRST002 for 15 and 14 s (two restores, 30 s and 41 s to healthy), 192 to 544 s (seven restores at 3.2 to 10.5 GB; the test machine slept during these runs and the lab has not yet checked which samples that removed, lab #211) | Measured, RP06 (R24, R25) and RP07 (R18, R19, R22, R23), service_role read up only on 200, 1 s probes, 2026-10-22 and 2026-10-23 |
Free pause -> INACTIVE 62-63 s; unpause -> healthy 183-212 s, Auth 152-175 s, Storage 181-217 s | Measured, RP03, 6 cycles (one outlier at 276 s and 444 s), 2026-10-10 |
Storage HTTP 500 on the first failing sample while a Free project paused | Measured, RP03, 2 of 3 runs, 2026-10-10 |
Pro has no pause (400 Project is not free-tier, project_pausing false) | Measured, RP01, one Pro organization, 2026-10-10 |
Restore to a new project, backups/restore | Not run (Dashboard route refuses a PAT; backups/restore answered 400 unavailable) |
| Current credentials reapplied after a physical restore | Documented10; the stored verifier differing from both values is an inference from fingerprints |
| Transfer: 2.3 s paid to paid, no outage above noise floor | Measured, polling /auth/v1/health every 250 ms, 2026-08-03 |
| Transfer: 75.2 s continuous paid to Free | Measured, same method, 2026-08-03; the cost is the arrival resize to Nano, not the transfer |
| Direct port 5432 | Not tested - IPv6-only, unreachable from this vantage |
| Upgrade window | Not tested - not constructible on a throwaway project |
| Why the restriction leaves reads alone | Not tested - inferred: the services reach Postgres from inside the platform |
Database-password PATCH: pg_postmaster_start_time() unchanged, zero failed samples on the HTTP-tier REST probe, Auth, Storage, the Realtime handshake | Measured, D05, 500 ms sampling over a 420 s window, one Micro project, n=1, 2026-10-01; the pooler was not probed |
Database-password PATCH, data path: postmaster_start unchanged, 0 failed rest_read (0 of 767, 0 of 154) and realtime_join (0 of 662, 0 of 132) samples | Measured, D05 re-run, two passes on one Micro project (420 s and about 83 s sampled), ap-southeast-1, 2026-10-23 |
| Medium to Large and back, data path: every path down 118 to 310 s in eight legs, Auth 128 to 221 s, REST read within 1 s of Auth | Measured, MS07 re-run, two Pro Medium projects (ap-southeast-2, ap-southeast-1), about 4 min and 1 h after create, n=1 per leg, 2026-10-23 |
Resize Micro -> Small -> Large -> Small -> Micro, data path: REST read max outage 231.465 / 223.28 / 152.466 / 254.886 s; ACTIVE_HEALTHY at 218 / 216 / 136 / 245 s | Measured, compute-disk D09 re-run, 250 ms sampling, one Pro project, n=1 per op, 2026-10-23; the 2026-08-19 D09 figures are withdrawn |
CLI config change: reload for a sighup setting, restart for a “Restart: Yes” parameter | Documented12; not measured |
| Platform restart and Postgres cumulative statistics | Not tested on the platform |
Reproducing
Section titled “Reproducing”Sample every path continuously while triggering the operation out of band, and record the failure mode beside the duration.
Recovery has to require sustained success rather than one passing sample, and the reported recovery time is the first sample of that sustained run, not the end of it. The pooler queues before it refuses, so a single lucky probe mid-outage otherwise registers as recovery.
A path already failing before the operation has to void that row, or a bad credential reads as a platform outage.
A probe timeout that is comparable to the sampling interval coarsens the resolution for whichever failure mode times out rather than fails fast - know which modes do which before quoting a duration as sub-second.
Modules
Section titled “Modules”References
Section titled “References”-
supabase-lab, “Data-path probes
rest_readandrealtime_join,” supabase-lab harness source (2026-10-23). https://github.com/erfianugrah/supabase-lab/blob/4141477/harness/src/real-probes.ts ↩ ↩2 -
supabase-lab, “Platform-downtime experiment,” supabase-lab runlog, section “2026-10-23 - data-path re-measure” (2026-10-23). https://github.com/erfianugrah/supabase-lab/blob/4141477/experiments/platform-downtime/RUNLOG.md ↩
-
supabase-lab, “Lifecycle-ops LO05 re-run with data-path probes,” supabase-lab run artifact (2026-10-23). https://github.com/erfianugrah/supabase-lab/blob/4141477/experiments/lifecycle-ops/out/2026-10-23/run-2026-10-23T02-47-19-611Z.facts.md ↩
-
supabase-lab, “Medium-serverless experiment,” supabase-lab runlog, section “2026-10-23 - MS02 and MS07 re-run with the data-path probes”. https://github.com/erfianugrah/supabase-lab/blob/fb257e4/experiments/medium-serverless/RUNLOG.md ↩
-
supabase-lab, “Compute-disk experiment,” supabase-lab runlog, section “2026-10-23 - D09 outage accounting fixed; a resize is minutes down, not seconds”. https://github.com/erfianugrah/supabase-lab/blob/fb257e4/experiments/compute-disk/RUNLOG.md ↩
-
Supabase, “Management API OpenAPI document,” api.supabase.com. https://api.supabase.com/api/v1-json ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Supabase, “Pricing,” Supabase. https://supabase.com/pricing ↩
-
Supabase, “Database backups,” Supabase Docs. https://supabase.com/docs/guides/platform/backups ↩
-
supabase-lab, “Restore-paths experiment,” supabase-lab runlog, “A correction (2026-10-23) to the rest path in RP02” and the RP07 table (2026-10-23). https://github.com/erfianugrah/supabase-lab/blob/4141477/experiments/restore-paths/RUNLOG.md ↩
-
Supabase, “Fixed stale database credentials left behind after a restore,” Supabase Changelog, 2026-07-30. https://supabase.com/changelog/restore-credential-resync ↩ ↩2 ↩3
-
supabase-lab, “Platform-downtime experiment,” supabase-lab runlog, section “2026-10-23 - D05 re-run with the data-path probes”. https://github.com/erfianugrah/supabase-lab/blob/fb257e4/experiments/platform-downtime/RUNLOG.md ↩
-
Supabase, “Customizing Postgres configs,” Supabase Docs. https://supabase.com/docs/guides/database/custom-postgres-config ↩ ↩2 ↩3
Linked from
- AWS PrivateLink to Supabase: an architecture reference
- OrioleDB on Supabase: what hosted projects do differently
- Per-project cost attribution on Supabase
- pg_cron on Supabase: restarts, resizes, COMMIT in a job and run history, measured
- Redundant writes in Postgres: what an unchanged upsert costs
- Running a platform on the Supabase Management API
- Supabase compute and disk sizing
- Supabase Data API connection pool and capacity, measured
- Supabase disaster recovery tiers: daily backups, PITR, warm standby - RPO, RTO, and cost
- Supabase incidents: what a client can do - a resilience reference
- Supabase preview-branch compute sizing & a CI parity model
- Supabase read replicas: how requests reach them and what they cost
- Supabase resilience runbook: probes, TTL levers, edge cache, warm standby
- Where should a tenant live: one Supabase project per tenant, or many