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. This page is where those costs live, so a plan that assumes “a restart takes about a minute” can be checked against what actually happened, on which path, before it is relied on.
TL;DR:
- REST and Realtime never failed under restart, either resize direction, or the network restriction. Auth, Storage and the pooler did, for different durations and by different failure modes.
- A resize costs 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.
- A network restriction reaches the pooler in ~1 s and never touches the HTTP tier. The reported 62 s window is mostly a chosen 60 s test dwell, not a platform duration.
- 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. - 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.
Answering means something different per path, and the difference matters most for the pooler, which carries the longest windows here.
| 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. The pooler’s rule is the strict one, and it is
not the same test. EADDRNOTALLOWED under a network restriction is a reply -
the pooler is reachable and refusing - but it is scored as a failure, because
what the probe asks is whether a query reached Postgres, not whether something
responded.
Recovery required five seconds of sustained success rather than one passing sample, but the recovered time reported is the FIRST sample of that sustained run, not five seconds later - the settle period confirms recovery, it 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”REST and Realtime never failed under any of the four operations sampled here. That is one observation, not two: whatever insulates the read path from a restart insulates it from a resize as well, and the paths do not move together
- a single duration for “a restart” or “a resize” throws away which path, for how long, and by what failure mode.
| operation | REST | Realtime | Auth | Storage | pooler |
|---|---|---|---|---|---|
| restart | - | - | 75 s | 78 s | 158 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 |
A dash is zero failed samples. The restriction row has no duration because its window is a test parameter rather than a platform property: the probe held the restriction for a fixed 60 s dwell and then removed it, so the 62 s it produced measures the dwell, not how long the platform takes to recover. What is measurable there is the 1 s to bite and the ~2 s to recover once the rule was withdrawn.
On the same restart that left Auth unavailable for 75 s, both REST and Realtime
served continuously at 500 ms resolution. Auth and Storage also fail
differently from each other, HTTP 521 against HTTP 500, so even the two
paths that behaved similarly did not behave identically.
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 is also not one number across operations:
| 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 |
The network restriction bit fastest, at 1 s. The restart bit at 2 s on every failing path. Both resize directions bit at 2 s on the HTTP tier and 3 s on the pooler. Nothing here 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 obvious reading is that the four HTTP services reach Postgres from inside the project and a client’s connection terminates at the service, while a pooled client’s socket sits much closer to what is being restarted or resized.
That is an inference from the result, not something measured here. It is drawn because it predicts the ordering, not because the topology was inspected.
Resize costs more than a restart, unevenly
Section titled “Resize costs more than a restart, unevenly”Auth was down 75 s restarting and 131 s resizing up: 131 s against 75 s is 1.75x. The pooler was down 158 s restarting and 207 s resizing up: 207 s against 158 s is 1.31x. A resize is not one constant multiple of a restart - it is 1.75x on Auth and 1.31x on the pooler, and the two ratios are far enough apart that neither stands in for the other.
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 close but not equal: 207 s up against 196 s is +5.6%, not within 5%. Whatever dominates the pooler’s window does not depend much on direction, but it is not direction-independent either.
Compute size turned out not to be a resize endpoint at all - it is an addon
mutation, PATCH on the project’s billing addons with an addon_type of
compute_instance1. 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. It is a database-socket control, not a project-wide lockdown.
It did reach the pooler, biting 1 s after sampling started, with a refusal that named the rejected address. The pooled port is covered by the restriction, not only a direct connection.
The pooler answers differently for each operation
Section titled “The pooler answers differently for each operation”| operation | pooler error |
|---|---|
| restart | Failed to connect to database: {:error, :timeout} |
| network restriction | (EADDRNOTALLOWED) address not in tenant allow_list |
| resize up | Failed to connect to database: {:error, :econnrefused} |
| resize down | terminating connection due to administrator command |
Only the last is a Postgres message; the others come from Supavisor. Across all four operations the pooler answered and reported the backend as unavailable in four different ways - unreachable, refused, deliberately shut down, or the client not permitted - rather than going away 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. What it does move is 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 ever located to within that interval, not to 500 ms.
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.
Fast-failing modes are unaffected: econnrefused, EADDRNOTALLOWED,
HTTP 521 and HTTP 500 all return immediately rather than waiting out the
timeout, so those windows keep 500 ms resolution throughout.
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 regardless of which side of the gap carries more slack. A claim that every number on this page shares one sampling resolution does not survive it.
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
schema1, so a created project always takes the current default -
there is no way to create one that has already aged past its version.
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 is a limitation of the rig, not a finding about the platform: it says nothing about how long an upgrade takes a client, only that this method cannot put a project into the state where the question applies.
What was readable 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 actually 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 is not writable: every one of the five projects refused its
first POST /auth/v1/admin/users with 500 unexpected_failure and accepted
the next attempt, one poll tick later. Provision-then-immediately-create-a-user
has to retry, because the first attempt failed on all five runs.
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 compute, 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’s cost landing on the
transfer, not the transfer’s own cost - a paid-to-paid move produces no
data-plane interruption distinguishable from measurement noise.
Reading the numbers
Section titled “Reading the numbers”n=1 per operation, except the restriction. One run each for restart and both resize directions, two runs for the network restriction, all on projects in a single organization. The ordering repeated across every operation and project tested, which is the part worth some confidence; the individual seconds are not.
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.
What the result supports: that these paths, on these operations, did not move together on this account, in this region, on these dates. What it does not: any figure for a different region, plan, project size, or point in time.
Evidence
Section titled “Evidence”| Claim | How it was checked |
|---|---|
| Restart: REST and Realtime 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 |
| Resize up 131/127/207 s, down 99/100/196 s | Measured, same probe set, same project, 2026-08-04 |
| Restriction leaves the HTTP tier serving | Measured twice, zero failed samples on four paths, 2026-08-04 |
| 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 document1 |
| A fresh project is at the latest version and cannot be created older | Read from the eligibility endpoint and the create schema1; nothing was upgraded |
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 |
| 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 HTTP tier is insulated | Not tested - inferred from the ordering |
Reproducing
Section titled “Reproducing”Sample every path continuously while triggering the operation out of band, and record the failure mode beside the duration. A few details decide whether the result means anything.
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 quietly 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.
References
Section titled “References”-
Supabase, “Management API OpenAPI document,” api.supabase.com. https://api.supabase.com/api/v1-json ↩ ↩2 ↩3 ↩4