Skip to content

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 = 5000 in probes.ts), so it carries multi-second resolution despite the rest of the matrix sampling at 500 ms.
  • Provisioning a fresh project to ACTIVE_HEALTHY took 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.

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.

PathCounts as answering
PostgREST, Auth, Storageany HTTP status other than a 5xx
Realtimethe 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.

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.
operationRESTRealtimeAuthStoragepooler
restart--75 s78 s158 s
network restriction----see below
resize up (micro -> small)--131 s127 s207 s
resize down (small -> micro)--99 s100 s196 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.

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:

operationbite time, HTTP tierbite time, pooler
restart2 s2 s
network restriction-1 s
resize up2 s3 s
resize down2 s3 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.

answered throughout / recovered in ~75-131sslowest path every timeclientPostgRESTAuthStorageRealtimeSupavisor:6543pooledPostgresdirect :5432NOT TESTED (IPv6 only)

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”
operationpooler error
restartFailed to connect to database: {:error, :timeout}
network restriction(EADDRNOTALLOWED) address not in tenant allow_list
resize upFailed to connect to database: {:error, :econnrefused}
resize downterminating 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.

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.

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.

OperationMeasuredMethod
Create project -> ACTIVE_HEALTHY131-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 sDELETE /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.

DirectionMeasured
Paid to paid2.3 s, no outage above the probing host’s noise floor
Paid to Free75.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.

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.

ClaimHow it was checked
Restart: REST and Realtime zero failed samples; Auth 75 s, Storage 78 s, pooler 158 sMeasured, 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 sMeasured, same probe set, same project, 2026-08-04
Restriction leaves the HTTP tier servingMeasured 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 stringsCaptured verbatim from the failing probe
Compute size is an addon mutationRead from the published OpenAPI document1
A fresh project is at the latest version and cannot be created olderRead from the eligibility endpoint and the create schema1; nothing was upgraded
duration_estimate_hours of 1 on three projectsRead from the eligibility endpoint; not an observed outage
Provisioning: create -> ACTIVE_HEALTHY 131-159 s, delete ~2 sMeasured, n=5, 2026-08-04, against a live organization
First admin/users write after healthy fails 500, retry succeedsMeasured, 5 of 5 projects, 2026-08-04
Transfer: 2.3 s paid to paid, no outage above noise floorMeasured, polling /auth/v1/health every 250 ms, 2026-08-03
Transfer: 75.2 s continuous paid to FreeMeasured, same method, 2026-08-03; the cost is the arrival resize to Nano, not the transfer
Direct port 5432Not tested - IPv6-only, unreachable from this vantage
Upgrade windowNot tested - not constructible on a throwaway project
Why the HTTP tier is insulatedNot tested - inferred from the ordering

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.

  1. Supabase, “Management API OpenAPI document,” api.supabase.com. https://api.supabase.com/api/v1-json 2 3 4