Skip to content

Supabase Data API connection pool and capacity, measured

How many database connections the Data API holds on a Supabase project, what a client gets back when every one of them is busy, what changing the pool costs, and which numbers the platform exposes about it. It is for whoever sizes a project for Data API traffic, sets db_pool or the statement timeouts, or alerts on the pool metrics, and wants the measured behaviour next to the documented one.

All measured rows come from the lab’s data-api-capacity experiment, run 2026-10-22/23 against throwaway projects on a Pro-plan organization, each created through POST /v1/projects and deleted at the end of its run. Modules DC01-DC03 ran four times in ap-southeast-1 on a project created Small and resized to Large; run 4 is the reference and its figures are quoted unless another run is named (runs 1 and 2 are published for their repeats, run 3 died in DC02 and wrote no artifact). DC04 ran once in ap-northeast-2 on a Small project resized to 4XL, DC05-DC07 once in ap-northeast-2 on Small, and DC08 once in ap-northeast-2 on a Small project resized to 4XL. The project region came from an environment variable and is recorded in the RUNLOG (and in DC08a’s artifact as the requested region); the artifacts’ region field is the harness default and was not read from the project. Every request in DC01-DC07 came from one Mac in Singapore over the public internet, so those client times include the round trip (p50 141 to 150 ms to Seoul at low load); DC08’s clients ran in the project’s region (below). PostgREST 14.18 and Postgres 17.11 were read in DC04a’s and DC08a’s artifacts; for DC01-DC03 they come from console output, and DC05-DC07’s artifact records neither. Most cells are n = 1 or n = 2; a row says when it is more. The figures are quoted from the RUNLOG and the redacted run artifacts listed in Modules. Rows marked documented come from the pages cited at the claim and were not run; rows marked source-read come from the PostgREST v14.18 source, and that the hosted build matches the tag was not checked.

TL;DR:

  • With db_pool unset the Data API pool was 30 connections on Small, 50 on Large and 70 on 4XL (4XL from pgrst_db_pool_max and an open-backend count, no burst), against max_connections 90, 160 and 480. GET /postgrest reads db_pool null; I found no published table of these figures.
  • A request behind a full pool answered 504 PGRST003 after 61364 and 61972 ms at the default 10 s acquisition timeout, and each left 6 PostgREST access-log lines. The reading is that a layer in front resent it five times; the public supabase/postgres Envoy config retries gateway errors 5 times, and whether hosted projects run it was not checked.
  • Every accepted PATCH /v1/projects/{ref}/postgrest restarted PostgREST (9 start lines for 9 PATCHes), a no-change PATCH included, and so did one that answered 500. With 60 s requests in flight the restart line came 55.3 s after, about when they finished (a reading, twice), and new requests got no answer for about 50 s.
  • Statement timeouts through the Data API: anon 3 s, authenticated 8 s, service_role 8 s inherited from authenticator. A function’s own set statement_timeout wins for every key, and alter role reaches the Data API only after notify pgrst, 'reload config'. All as documented.
  • One indexed point read from one Mac, anon key: Small stopped at 853 to 1,020 req/s from 256 concurrent clients on; 4XL reached 3,158 req/s at 512, then the client (os error 51) and 403s from an unidentified layer stopped the measurement. db_pool 15, 30 and 60 on Small gave 853, 902 and 909 req/s at 256, a difference not separated from the spread between repeats.
  • In-region Edge Function clients with a mixed load (point, range, RPC, insert, count=exact) took 4XL to 6,148 req/s at c=3072, a step that added 5 % for about 25 % more requests in flight (derived; one run, one step, nothing above it run), with at most 7 of 71 authenticator backends active in that step (14 at most in the ramp). db_pool 150 and 250 did not raise throughput or lower p50 there, and p95 and p99 were higher (one step each). The generator under-delivered its requested concurrency.
  • pgrst_db_pool_waiting counts a request from its pool request until its session ends, so it includes requests holding a connection and PostgREST’s own pool sessions (schema cache, config reads), and pgrst_db_pool_available is open connections minus those in use (source-read; the snapshots agree). The series are a snapshot: no refresh landed in a 36 s window, and the pool max showed a PATCHed value after 1.4 s once and 28.9 s once. One 4XL snapshot read 1,078 waiting with 58 of 70 connections available at some moment in the first 33 s of the c=3072 step, and still read so 25 s after the load ended (n = 1).
  • 60 statement-timeout cancellations caused by a 12.6 s table lock left no aborted pooled connection: 9920 requests answered 200 afterwards, 0 answered 25P02 (one run per arm, anon reads, 20 workers on a pool of 30, so nothing was queued for the pool).

client(one request)platform layer in front(unidentified)PostgRESTpool: db_poolwait: db_pool_acquisition_timeout6 attempts per lonerequest (reading)Postgresauthenticator backends(max_connections)pooled connections/customer/v1/privileged/metricspgrst_db_pool_*snapshot

Text version: a client request passes a platform layer before PostgREST; PostgREST takes a connection from its pool (db_pool connections, each an authenticator backend in Postgres) or waits up to db_pool_acquisition_timeout for one; after a PGRST003 the same request reached PostgREST six times in total, which reads as resends by the layer in front; the pool series reach the project’s metrics endpoint as a snapshot.

What the client seesWhat it was in these runsLever
504 PGRST003 after about 6 x the acquisition timeoutevery pooled connection busydb_pool, db_pool_acquisition_timeout, shorter requests, compute size
503 PGRST002 that persists after the restart line following a db_pool raisedb_pool 1000 against max_connections 160a db_pool that fits in max_connections
503 PGRST002 within about 2.7 s while another client (Hyperdrive) holds the slotsPostgres refusing PostgREST a new connection; pool rebuilt about 8 s later (hyperdrive-direct H03)origin connection limits that fit; see the connection budget
500 57014 at about 3 s or 8 sthe role’s statement timeouta function-level set statement_timeout, or alter role plus a config reload
no answer for about 50 s just after a config PATCHPostgREST restarting behind long in-flight requestsschedule PATCHes, or PATCH when no long requests run
flat req/s while latency doubles per concurrency step, on Smallthe Small project’s throughput ceiling; pool sizes 15 to 60 not separated from noise (DC07)a larger compute size (4XL scaled)
req/s up 5 % for about 25 % more requests in flight (derived), most authenticator backends idle, on 4XLone DC08 run, one step: requests queueing for a fixed service rate, with no sign of the queue in Postgres; the limiting layer not identifiednone found; db_pool 150 and 250 gave the same p50 (p95/p99 higher, one step each)

The Management API documents db_pool as “If null, the value is automatically configured based on compute size” and db_pool_acquisition_timeout as “If null, the value is automatically configured to 10”.1 Upstream PostgREST defaults db-pool to 10 and db-pool-acquisition-timeout to 10 seconds.2 The Supabase compute page lists Postgres max connections and pooler clients per size, not the Data API pool;3 I found no published table of the pool per size in that page, the connection management guide4 or the Management API spec, read 2026-10-23.

The lab counted the pool as the number of authenticator backends (the Data API’s login role4) sleeping in PgSleep in the middle of a burst of long RPCs larger than the pool.

Sizemax_connectionsPool, db_pool unsetHow countedRuns
Small9030two bursts of 100: 30 / 30 sleeping, 31 authenticator backends in total; pgrst_db_pool_max 30 (DC04a)runs 1, 2 and 4 (DC01a), DC04a
Large16050two bursts of 140: 50 / 50 sleeping, 51 in totalruns 1 and 4 (DC01d)
4XL48070pgrst_db_pool_max after the resize (DC04c, DC08a); 71 authenticator backends open under load from c=512 (DC08b); no PgSleep burst counttwo runs (DC04c, DC08)

GET /postgrest read db_pool null and db_pool_acquisition_timeout 10 (a number, not null) on Small and on Large, and db_pool null on 4XL. The max_connections figures match the compute page.3 The one authenticator backend outside the pool count is unidentified (see the metric section).

A burst that drains within about six acquisition timeouts came back as 200s: 100 concurrent 12 s requests on Small’s 30 (four waves, the last waiting about 36 s against a 10 s acquisition timeout) all answered 200, as did 140 on Large’s 50. That fits the resends below; PostgREST log lines were not counted for bursts. 220 concurrent 15 s requests on a pool of 50 did not: 150 answered 200 and 70 answered 504 PGRST003 (DC02c).


PostgREST documents that a request waits for a pooled connection for at most db-pool-acquisition-timeout seconds and that the timeout answers 504 PGRST003, “The request timed out waiting for a pool connection to be available.”25

DC01b and DC02b held every pooled connection with long sleeps, then sent single POST /rest/v1/rpc requests with the service_role key, each with its own User-Agent, and counted the PostgREST access-log lines carrying that User-Agent. The client was a plain fetch with no retry logic and a fresh connection per request.

Acquisition timeoutPool heldClient’s time to the answerAnswerPostgREST log lines per request
10 s (default), run 430 of 30 sleeping61364 ms, 61972 ms504 PGRST003, both6 (504:6), both
10 s, run 13061343 ms, 61493 ms504 PGRST0036 each
10 s, run 23061972 ms, 61795 ms504 PGRST0036 each
2 s90 of 90 sleeping13767 ms, 13752 ms504 PGRST003, both6 (504:6), both
4 s90 of 90 sleeping25007 ms504 PGRST0036 (504:6)
4 s, second request90 of 90 sleepingno answer by the client’s 105001 ms timeoutstatus 00, not explained

With no separate probe, one client request produced six PostgREST access-log lines, all 504, and the client’s wait was six acquisition timeouts plus the round trip (60 s against 61364 ms, 12 s against 13767 ms, 24 s against 25007 ms). The reading is that something between the client and PostgREST sent the request again after each PGRST003, six attempts in total. The competing reading, that PostgREST logs one request several times, was not separated, since no request id was traced through PostgREST. The 4 s figure rests on one request.

A public candidate for that layer is the Envoy configuration in the supabase/postgres image repository. Its virtual host carries a retry policy of num_retries: 5 with retry_on: gateway-error and sets include_attempt_count_in_response: true, and the /rest/v1/ routes carry no retry policy of their own;6 Envoy’s gateway-error policy retries on 502, 503 and 504, and include_attempt_count_in_response returns the attempt count to the client in x-envoy-attempt-count.7 Five retries on a 504 is six attempts, and a 500 is outside the policy, which fits DC03d below. That hosted projects run this configuration was not checked, and the attempt-count header was not read in these runs.

Under load the wait was shorter. In DC02c’s 220-request control at a pool of 50, the 70 PGRST003 answers came after 10923 to 20903 ms (median 20855), one or two acquisition timeouts. Attempts were not counted inside the burst, so how the resends behave under load is not separated. One candidate in the same repository is the retry_budget on Envoy’s postgrest cluster (budget_percent 100, min_retry_concurrency 100),8 which caps concurrent retries at the sum of active and pending requests and never below 100;9 whether it, or something else, shortened the waits is not established.

A 500 was not sent again. DC03d made an RPC with a side effect (a nextval) run past the 8 s authenticated timeout: 500 57014 after 8058 and 8296 ms, one execution per request by the sequence, and one PostgREST log line each (500:1). The resend covered the PGRST003 probes and not this 500. A client that retries a 504 itself adds its attempts on top of the six (inference from the counts above; client retries were not run).

The Management API spec gives db_pool a range of 0 to 1000 and db_pool_acquisition_timeout 0 to 60 on the update body.1 Values sent outside them (DC02c):

SentAnswer
db_pool 1001400 db_pool: Too big: expected number to be <=1000
db_pool -1400 db_pool: Too small: expected number to be >=0
db_pool_acquisition_timeout 61400 db_pool_acquisition_timeout: Too big: expected number to be <=60

A 0 for either was not sent. For 70 and 90 on Large, the PATCHed value is what Postgres saw: both read back and a burst then held 70 and 90 authenticator backends in PgSleep (DC02a).

DC02c PATCHed db_pool 1000 on Large (max_connections 160); the PATCH answered 200 and read back 1000. The same burst of 220 concurrent 15 s requests as the control then gave:

db_poolAnswersError body
50 (control, PATCHed back just before)150 x 200, 70 x 504 PGRST003PGRST003
1000154 x 200, 66 x 503 PGRST002"Could not query the database for the schema cache. Retrying."

PostgREST documents PGRST002 as “Could not connect with the database when building the Schema Cache due to the PostgreSQL service not running.”5 The reading: PostgREST tried to open more connections than Postgres accepted and the refused connection surfaced as PGRST002 instead of a wait. It is one burst per arm, and no connection count near 160 was observed: the mid-burst read through the Management API query endpoint returned no row (its status and error were not recorded). After the burst, 5 consecutive good pings took 6 s against the check’s floor of about 4 s, so no outage after the burst was measured.

The same 503 PGRST002 appears when another client fills max_connections first. In the lab’s hyperdrive-direct H03 (RUNLOG; 9 arm-runs on Small across three runs), Hyperdrive origin connections held the slots: as many RPCs answered 200 as PostgREST had pool connections plus free slots, every other one answered 503 PGRST002 in 719 to 2718 ms, and PostgREST initialised a new pool and reloaded its schema cache 7.6 to 8.4 s after the burst. How to budget connections when Hyperdrive and the Data API share a project is in Cloudflare Workers + Supabase.


DC02e counted 9 “Starting PostgREST” lines in the project’s PostgREST logs (postgrest_logs) for 9 accepted PATCHes to /v1/projects/{ref}/postgrest, one of them a PATCH that changed nothing. The sibling data-api-reenable experiment recorded the same for a no-change PATCH (DA02 in its RUNLOG), and turning the Data API back on is another PATCH that restarts it (Locking down Supabase). PostgREST marks db-pool and db-pool-acquisition-timeout “Reloadable N”, so a new value needs a process start to apply.2 DC04d adds a case: the PATCH of db_pool 250 on 4XL answered 500, and a restart line still followed 1.1 s later. DC08c’s three PATCHes (150, 250, 70) each answered 200 and each was followed by a restart line, after 1.8, 1 and 1.4 s; its PATCH to 250 answered 200.

PATCHRestart line after the PATCHData API requests during the restart
db_pool 70, then 90 (DC02a)0.7 s, 0.7 s0 of 357 and 0 of 358 pings failed, one every 250 ms for 90 s
acquisition timeout 2, then 4 (DC02b)0.9 s, 0.7 snot sampled
db_pool 50, 1000, 50 (DC02c)0.8 s, 0.7 s, 0.5 sdb_pool 1000: 0 failed
db_pool 20, 30 (DC05c)1.2 s, 1 snot sampled
db_pool 15, 30, 60 (DC07)1.1 s, 1.5 s, 1.3 snot sampled
no-change db_pool with 20 requests sleeping 60 s in flight, twice (DC02d)55.3 s, 55.3 s20 of 20 in flight answered 200, both times; 5 of 267 and 5 of 265 pings failed (status 0 at the client’s 10 s timeout), first at 1.3 s and 0.8 s, back to back for about 50 s

With nothing long in flight the restart came within about 1.5 s and no sampled request failed. With 60 s requests in flight (sent 5 s before the PATCH) the restart line came 55.3 s after the PATCH, about when they finished; all of them completed, and new requests got no answer for about 50 s, ending before the logged restart. That fits a restart that drains in-flight requests before stopping and holds new requests behind it; no separate probe was run.

Run 1 sent its PATCHes without waiting for the restart line, and three of its rows look like restart overlaps (a burst where 218 of 220 requests ended on a closed socket at the client, among them). From run 2 on, DC02 waits for the restart line after every PATCH before measuring.


Documented: anon 3 s, authenticated 8 s, service_role “none (defaults to the authenticator role’s 8s timeout if unset)”, and a role change needs NOTIFY pgrst, 'reload config' to reach the Data API.10 DC03 checked each of those through the Data API with RPCs that sleep.

Role, how the request was maderolconfigSleepAnswer
anon (anon key){statement_timeout=3s}5 s500 57014 after 3062 and 3057 ms
authenticated (anon key plus a user token){statement_timeout=8s}10 s500 57014 after 8065 and 8048 ms
service_role (service_role key)null10 s500 57014 after 8056 and 8048 ms
authenticator (PostgREST’s login role){"session_preload_libraries=supautils, safeupdate",statement_timeout=8s,lock_timeout=8s}--

The error body was {"code":"57014","details":null,"hint":null,"message":"canceling statement due to statement timeout"}.

ChangeAnswer
A function declared set statement_timeout = '30s', 12 s sleep, each of the three keys twice200 in every case, 12067 to 12105 ms
alter role authenticated set statement_timeout = '15s', user token, 10 s sleep, right after the ALTER500 57014 after 8078 and 8064 ms
the same after notify pgrst, 'reload config'200 after 10076 and 10103 ms
the same after notify pgrst, 'reload schema' as well200 after 10098 and 10080 ms
back to 8 s plus a config reload500 57014 after 8074 ms

service_role carries no timeout of its own, so a Data API request under it runs with the 8 s that authenticator logs in with. A function’s own setting applied to the call for every key, anon’s 3 s included. An alter role reached the Data API only at the next config reload. Every row agrees with the timeouts guide.


DC01-DC03 used requests that hold a connection for seconds. DC04 and DC07 used request rate instead: oha (1.16.0, from console output) in a closed loop, 40 s per step, one GET /rest/v1/dc_items?id=eq.4242&select=id,payload (an indexed point read on a 100,000-row table) with the legacy anon key, the pool series scraped every 3 s, node CPU from node_cpu_seconds_total and the authenticator states read once mid-step. Success is 200s over all attempts, client-side errors included. DC08 moved the clients into the region and used a mixed load (below). The compute page lists Small as shared CPU and 4XL as 16 dedicated vCPUs.3

Concurrencyreq/sp50 / p99 msSuccessPool waiting max (available then, of max)Node CPU %authenticator mid-stepClient errors
16108143 / 37410 (1 of 30)not computedidle:7none
32214144 / 34511 (5 of 30)9.5idle:7none
64428145 / 33411 (5 of 30)21.7active:1 idle:18none
128825149 / 37017 (11 of 30)49.1idle:16 idle in transaction:3none
256925269 / 5261139 (11 of 30)73.9idle:16 idle in transaction:32
512918547 / 10390.993139 (11 of 30)not computedidle:27 idle in transaction:4277
102410201110 / 17480.8720 (30 of 30)not computedidle:22 idle in transaction:95,323

From 256 on, throughput stayed at about 920 to 1,020 req/s while latency doubled per step, which is what a closed loop does against a fixed ceiling. In the 256 step one snapshot read 139 waiting with 11 of 30 connections available (the step’s median was 7 waiting). Since waiting includes requests holding a connection (below), and at most 30 minus 11 connections were in use, at least 120 of those 139 had no connection while 11 open ones sat idle, if the payload was internally consistent. The 512 step showed the same 139 and 11 on every scrape, so it is one snapshot across two steps; node CPU comes from the same endpoint and is a snapshot too, which is the condition on the 73.9 % figure. Node CPU covers both Postgres and PostgREST if they share the node, as the project image suggests (it installs PostgREST as a service beside Postgres;11 hosted placement not checked), so it does not say which process used the CPU; no per-process figure was read. The 1024 row reads nothing waiting while 12.8 % of requests failed on the client.

Concurrencyreq/sp50 / p99 msSuccessPool waiting max (available then, of max)Node CPU %StatusesClient errors
64436141 / 36510 (1 of 70)not computed200:17521none
128860143 / 33910 (1 of 70)2.2200:34810none
2561699144 / 35511 (10 of 70)5200:687731
5123158150 / 4570.9961 (10 of 70)11.5200:127297456
10242243164 / 21620.8370 (66 of 70)11.5200:75566 403:680416,018
1536539801 / 44600.3750 (66 of 70)0.1403:885614,729

On 4XL the same request scaled linearly to 3,158 req/s at 512, with the snapshots reading at most 1 waiting and 10 of 70 available. Node CPU read 11.5 % at both 512 and 1024, which fits a stale window. Above 512 the run stopped measuring PostgREST: the client logged “Network is unreachable (os error 51)” on thousands of requests, and from 1024 the answers included HTTP 403 (every answer at 1536) while node CPU fell to 0.1 %. The 403 body was not recorded, so which layer sent it is not established. Those two rows describe one Mac and the edge in front of the project, and DC08 on the same size reached 6,148 req/s from in-region clients with a different request mix. DC04d’s db_pool arms on 4XL (20, 150 and 250) ran behind the same limit and compare nothing about pool size; each arm’s pool max read the previous arm’s value, and the PATCH to 250 answered 500 and was not investigated.

4XL with in-region clients and a mixed load (DC08)

Section titled “4XL with in-region clients and a mixed load (DC08)”

DC08 moved the clients into the project’s region and replaced the single point read with a mix. The lab had no machine of its own in the region that day, so the clients were Supabase Edge Function invocations of a generator deployed to the project under test and pinned to ap-northeast-2 with the x-region header; this lab’s Mac fired each round’s invocations after one warm-up call per client. Every invocation that reported its region reported ap-northeast-2, and every sampled (1 in 50) Data API response carried a cf-ray from Cloudflare’s ICN (Seoul) colo. One invocation is one closed-loop client of 8 workers for an 8 s round; a step is 6 rounds (48 s), and the concurrency label is clients x 8. Every request used the legacy anon key against the 100,000-row table, mixed by weight: a point read 50, a 50-row range read 15, an RPC returning one indexed row as JSON 15, an insert with Prefer: return=minimal 10, and a Prefer: count=exact read that counts 10,000 rows and returns 20, 10.

The generator under-delivered. Invocations failed with 546 WORKER_RESOURCE_LIMIT, 503 BOOT_ERROR and 429 RATE_LIMIT_EXCEEDED, a failed invocation was retried while 2 s of its round remained, and observed starts were up to 16.2 s late, so each step’s concurrency label overstates the load. “Clients answered” is the invocations that returned a result, per round. Requests in flight (req/s x p50, derived, client-side and including edge and gateway time) are the closest the run has to the delivered load. The generator in this run took its target host from the request; after the run it was changed to target only the project it is deployed in, and that version has not been run.

Concurrency (clients x 8)Clients answered per roundreq/sp50 / p95 / p99 msauthenticator active / idle / idle in transaction (range in load, from the RUNLOG; the artifact holds all-sample min/median)Pool snapshots waiting/available/maxPostgres exec ms/s
64 (8)3.898918.6 / 30.7 / 47.20-1 / 9-13 / 0-40/2/70258
128 (16)9.5185719.2 / 31.8 / 47.70-7 / 20-33 / 0-40/13/70622
256 (32)17.2311019.9 / 34 / 590-6 / 37-71 / 0-160/32/701057
512 (64)34.5396241.9 / 63.8 / 85.30-14 / 50-71 / 0-140/70/701491
1024 (128)88.84806125 / 163 / 2650-9 / 56-68 / 3-150/70/701256
2048 (256)2375844230 / 312 / 5120-6 / 49-66 / 2-220/70/70777
3072 (384)365.76148273 / 320 / 3840-7 / 58-64 / 6-130/70/70, then 1078/58/70 from 33 s801

Every answer in the ramp was 200, 201 or 206 (every count=exact answer was a 206, the Partial Content status PostgREST documents for a counted range12), with no 403 and no 5xx, and every op had the same latency within a step: at c=3072 the per-op p50 was 273 to 275 ms, the count=exact read included. Throughput rose 27 %, 21 %, 22 % and 5 % per step from 3,110 req/s at c=256 to 6,148 at c=3072 while p50 went 19.9, 41.9, 125, 230 and 273 ms, and requests in flight were about 62, 166, 600, 1,340 and 1,680. Above c=512 latency grew roughly with the requests in flight, and the last step added 5 % for 25 % more of them. That reads as a queue in front of a fixed service rate; it is one step, and nothing above c=3072 was run.

The queue showed no sign of being in Postgres. In the samples taken during load at most 14 authenticator backends were active; from c=512 all 71 were open (the pool of 70 plus the extra idle backend DC05 counted), with 49 or more idle. Executor time was at most 1,491 ms per second of the window, about 0.1 ms per call (derived), on 16 vCPU. The Postgres columns are pg_stat_statements deltas over a window that ran past the load (to 73-75 s at c=2048 and c=3072), and calls per client request fell from about 4 at c=64 to 1.3 at c=3072, which the window alone does not explain; read them as a floor within a step, not as a comparison between steps. Node CPU, each figure across one payload refresh at a moment not recorded, read 49.6 % at c=2048 and 29.9 % at c=3072; which process used it was not measured. The generator’s event-loop lag p99 had a median of 2 to 9 ms per step and its per-client rate fell as latency rose, so its workers mostly waited on answers; its outbound connections were not measured. The edge and the API gateway between the clients and PostgREST were not instrumented, and a limit there would look the same from here.

Pool size under load (DC07 on Small, DC08c on 4XL)

Section titled “Pool size under load (DC07 on Small, DC08c on 4XL)”

DC07 set db_pool to 15, 30 and 60 on a Small project, waited for the restart line and 30 s more, then ran DC04’s step at 128, 256 and 512. Every PATCH answered 200 and read back the value set. Every HTTP answer in every step was a 200; the failures in the success column are the client’s (mostly os error 51 at 512).

db_poolConcurrencyreq/sp50 / p99 msSuccessauthenticator mid-stepClient errors
15128814147 / 4221idle:12 idle in transaction:4none
15256853288 / 6671idle:15 idle in transaction:11
15512863615 / 10730.992idle:16280
30128834147 / 3761idle:28 idle in transaction:1none
30256902281 / 4700.998idle:28 idle in transaction:160 timeouts
30512878618 / 8630.985idle:27 idle in transaction:2516
60128836147 / 3481idle:35none
60256909277 / 5791idle:31 idle in transaction:417 timeouts
60512868615 / 9400.993idle:34 idle in transaction:1238

At 256 concurrent the three pools gave 853, 902 and 909 req/s, a spread of 56 req/s from pool 15 to pool 60. The same configuration on two runs (pool 30 on Small, DC04b’s 925 and 918 against DC07’s 902 and 878 at 256 and 512) differed by 23 and 40 req/s, so the pool effect is not separated from the spread between repeats. The pool-15 arm also ran with a busier client: the Mac’s 1-minute load average read 8.8, 12.9 and 11.7 in its steps against 3.6 to 4.9 in the others. The same Mac reached 3,158 req/s against 4XL the same day (DC04c). Mid-step, most authenticator backends were idle at every pool size, and the 60 pool never showed more than 35 of them. Node CPU was not computed in any DC07 step, because the CPU counters did not change within a 40 s step; with steps about 60 s apart and a slow payload refresh, that says little. Each cell is one step with no repeat. Pool 15 was the lowest of the three at every step, and its arm is the only one whose snapshot showed queueing; with one step per cell and a busier client, a small penalty and noise were not separated.

After the ramp, DC08c set db_pool to 150, then 250, then back to 70 on 4XL, each followed by the c=3072 step after the restart line, a readback and 20 s. Every PATCH answered 200 and read back the value; the restart line came 1.8, 1 and 1.4 s after.

db_poolClients answered per roundreq/sp50 / p95 / p99 msauthenticator active / idle / idle in transaction (range in load, from the RUNLOG; the artifact holds all-sample min/median)Pool snapshots (scrapes)
unset, 70 (ramp)365.76148273 / 320 / 3840-7 / 58-64 / 6-130/70/70, 1078/58/70 (16)
1502994830281 / 362 / 4900-4 / 133-149 / 2-180/51/150 (4)
2503605901278 / 353 / 4620-5 / 173-240 / 7-780/0/250 (5)
70368.86210274 / 328 / 3790-6 / 51-61 / 5-140/70/70 (12)

A bigger pool did not raise throughput or lower p50 at that load: p50 was 273, 281, 278 and 274 ms at pools 70, 150, 250 and 70, and p95 and p99 were higher at 150 and 250. Each arm is one step with no repeat; the two pool-70 steps differ by 62 req/s. The pool-150 arm’s lower rate came with fewer clients answering (299 per round against 360 to 369; 394 invocations ended with status 0 on the Mac that fired them (error text not recorded in that arm), about one round of 384), so its rate measures the generator, and its p50 matched the other arms. With more connections, more sat idle: 133 to 149 at pool 150 and 173 to 240 at 250, with at most 5 active. None of the arms’ pool tuples changed during the step, so whether requests queued for a connection at 150 or 250 is not shown, and 0/0/250 is not 250 connections in use (the samples beside it read 173 to 240 idle). The last pool-70 arm had three 502s; the one body kept was Cloudflare’s 502 Bad Gateway page. The PATCH to db_pool 250 answered 200 here, where DC04d’s PATCH to 250 answered 500 (not investigated).

Upstream, PostgREST issue #5336 (open as of 2026-10-23) reports throughput collapsing when requests wait for a pool connection: in hasql-pool, every returned connection wakes every waiting request, so PostgREST’s CPU per request grows with the number waiting.13 Its benchmark ran PostgREST main on an AWS Graviton3 host with Postgres pinned to 2 cores, db-pool 20 and 100 closed-loop clients, and the collapse grew with PostgREST’s core count: 5,714 req/s at 293 microseconds of CPU per request on 2 PostgREST cores, 9,649 req/s at 324 microseconds on 4, 3,366 req/s on 8 and 1,684 req/s on 16. The issue gives a pool large enough that nobody waits as the workaround and points to a fix proposed in hasql-pool PR #58. Those are the issue’s own measurements under its conditions; the lab did not reproduce them. The issue’s 2- and 4-core rows with Postgres on 2 cores show CPU per request near its no-wait figure (293 and 324 microseconds against 333); its 4-core pool contrast with Postgres on 1 core shows pool 20 at 2,768 req/s against 6,875 at 200. Which regime a Small project is in was not established: how many cores PostgREST gets there is not documented, and whether DC07’s 30 and 60 arms queued at all was not observed. The issue measured the hasql-pool copy vendored on PostgREST main; v14.18 takes hasql-pool from Hackage at >= 1.0.1 && < 1.1,14 and whether that version has the same acquisition code was not checked. DC08c on 4XL, flat throughput with most connections idle at pools of 70, 150 and 250, fits a limit in PostgREST that a pool below the requests in flight does not lift. The issue’s workaround was not tested there: 250 is below the 1,360 to 1,700 requests in flight in those arms (derived), and the run does not separate a limit in front of PostgREST.


The project metrics endpoint, /customer/v1/privileged/metrics (scrape set-up in Monitor Supabase with Grafana), served 9 pgrst_* series in DC04a: pgrst_db_pool_available, pgrst_db_pool_max, pgrst_db_pool_timeouts_total, pgrst_db_pool_waiting, three JWT cache series and two schema cache series. PostgREST’s help text calls pgrst_db_pool_available “Available connections in the pool” and pgrst_db_pool_waiting “Requests waiting to acquire a pool connection”.15

The v14.18 source counts differently from that help text. usePool raises the waiting gauge before it asks the pool for a connection and lowers it after the session using that connection returns,16 so waiting counts requests from their pool request until their session ends: requests queued for a connection plus requests holding one. PostgREST’s own pool sessions go through the same function (the connection check when the listener is off, the schema cache load, the in-database config and role-settings reads), so they count too.17 available is computed as open pool connections minus connections in use.18 This is source-read; the Supabase build is assumed to match the tag, which was not checked. DC05 read the series against pg_stat_activity for usename = 'authenticator', read through the Management API query endpoint, and the snapshots agree with the source.

1 s scrapes for 100 s while held requests stepped 0, 5, 15, 25 and 0 every 20 s; pg_stat_activity about every 2.5 s.

MeasuredValue
scrapes100, 0 failed
distinct waiting/available/max readings2: 0/1/30 from 0 s to 60 s, 25/0/30 from 62 s to 99 s
changes1
response headers, first scrapecf-cache-status=DYNAMIC, date current
authenticator total / in PgSleep / idle2/0/2 to 20 s; 6/5/1 from 22 s; 16/15/1 from 41 s; 16/0/16 at 58 s; 26/25/1 from 61 s; 26/0/26 from 81 s

The series moved once in 100 s while the backends moved five times. No refresh landed between 22 s and 58 s, a 36 s window in which the 5- and 15-held states existed and never appeared, and none between 81 s and 99 s, after the holds ended. A periodic refresh would therefore be at least about 40 s apart (the change at 62 s, the previous refresh no later than 22 s); no upper bound was measured. The endpoint answered DYNAMIC with a current date, so the staleness sits behind the edge.

10 then 40 requests held for 30 s on a pool of 30; five scrapes 1.5 s apart from 12 s into the hold, pg_stat_activity at the third.

HeldScrapes (waiting/available/max)authenticator total / in PgSleep / idleavailable if it meant open idle / max minus checked outHolds
1025/0/30 x4, then 10/15/3026 / 10 / 1616 / 20200:10
4010/15/30 x531 / 30 / 11 / 0200:40

Three snapshots line up with a known state:

  • available read 15 with 10 held against 16 idle authenticator backends, 1 at rest (DC05a, 0 s) against 2 idle, and 0 with 25 held (DC05a, 62 s) against 1 idle. That matches open connections minus those in use. In those three, one idle backend was not counted, which fits one connection outside the pool (PostgREST’s listener; not identified). “Max minus checked out” would have read 20, 30 and 5.
  • waiting read 25 with 25 held and 10 with 10 held, on a pool of 30 where nothing queued: the requests holding a connection, as the source predicts. DC07 shows the queued part too: with db_pool 15 at 128 concurrent, one snapshot read 31 waiting against a pool max of 15.
  • The 40-held row is the 10-held snapshot still being served. The queued state, 30 in PgSleep and 10 waiting for a connection, was never scraped.

Under load on 4XL (DC08b) one tuple showed requests queued while connections sat free. The tuple first scraped 33 s into the c=3072 step (the 25 s scrape read 0/70/70) read 1,078 waiting with 58 of 70 available. Holding is at most 70 - 58 = 12, so at least 1,066 requests were queued for a pool connection inside PostgREST at some moment between load start and 33 s while 58 open connections were idle; given the lags DC05 measured, the tuple does not say when. The authenticator samples in that step read 0 to 7 active and 58 to 64 idle. The tuple still read 1,078 at 73 s, 25 s after the load ended, when the samples read 0 active and 71 idle, and the 14 scrapes that show it are one tuple (n = 1). At c=2048 and in the last pool-70 arm the node CPU counters moved, so the payload refreshed at least once, and the pool tuple still read 0/70/70; when those refreshes landed relative to the load is not recorded, so whether the queue was there at earlier steps is not settled.

PATCHStatusRestart line afterpgrst_db_pool_max read the new value after
db_pool 20 (from unset, 30)2001.2 s1.4 s, first scrape
db_pool 302001 s28.9 s, 22 scrapes

PostgREST restarted within about 1 s both times; the metric followed after 1.4 s once and 28.9 s the other time. The 28.9 s case fits the slow refresh above. The 1.4 s case puts the refresh within a fraction of a second of the logged restart (client and log clocks, skew not measured), so the competing reading, that a restart resets or refreshes the payload, is open; the two were not separated. Either way it fits DC04d, where each arm read the previous arm’s pool.

For an alert or a capacity decision, count the pool where it lives before acting on these series. The lab’s own reading was a pg_stat_activity query of this shape, run through the SQL editor or the Management API query endpoint:

select count(*) filter (where usename = 'authenticator') as total,
count(*) filter (where usename = 'authenticator' and state = 'idle') as idle,
count(*) filter (where usename = 'authenticator' and state = 'active') as active
from pg_stat_activity;

Statement timeouts from a lock, then the pool

Section titled “Statement timeouts from a lock, then the pool”

DC06 asked whether requests cancelled by a statement timeout leave pooled connections in a broken state. 20 workers looped anon GETs on dc_items while a Management API query ran begin; lock table public.dc_items in access exclusive mode; select pg_sleep(N); commit;, with 75 s observed after the lock and authenticator states read about every 5.7 s. Anon’s 3 s timeout is the DC03a figure, from a different run and project. The 2 s control ran first; each arm ran once.

Lock asked / heldBeforeDuring (to 4 s after the lock)After (4 s to 75 s)25P02 afterMost idle in transaction (aborted) seenRequests
2 s / 2.7 s200:2034200:664200:98570012555
12 s / 12.6 s200:2050200:643, 500 57014:60200:99200012673

During the 12 s lock the states read active:20 idle:1 twice, then idle:21 from 1.4 s after it, with at most 2 idle in transaction (not aborted) in any later sample. 60 requests were cancelled by anon’s statement timeout while waiting on the lock, and none of the pooled connections they used was left aborted: every request after the lock answered 200 and no sample showed an aborted backend. With 20 workers on a pool of 30 no request waited for a pool connection, so a cancellation while queued was not exercised. A stall where Postgres stops as a whole, with cancellations landing at a different protocol step, was not made: the lab has no way to stop Postgres as a whole on a managed project.


ClaimDocumentedMeasured or source-readFiled upstream
Time to PGRST003the request waits up to db-pool-acquisition-timeout, then 5042a lone request answered after about 6 x that timeout, with 6 PostgREST log linesnot filed; the lab RUNLOG records no report
Pool with db_pool null“automatically configured based on compute size”1Small 30, Large 50, 4XL 70; no published figure foundnot filed
pgrst_db_pool_waiting“Requests waiting to acquire a pool connection”15source: queued plus holding a connection; snapshots read 25 and 10 with nothing queuednot filed
pgrst_db_pool_available“Available connections in the pool”15source: open connections minus in use; snapshots one fewer than the idle authenticator backendsno disagreement
Role statement timeouts and reloadanon 3 s, authenticated 8 s, service_role from authenticator 8 s, reload config needed10the sameno disagreement

ClaimMeasured or documentedHow it was checked
Pool 30 / 50 with db_pool unset on Small / Largemeasured, runs 1, 2 and 4 (Small), 1 and 4 (Large)authenticator backends in PgSleep mid-burst, two bursts per size
Pool 70 on 4XLmeasured, two runs, metric plus backend count under loadpgrst_db_pool_max after the resize; no burst count
PGRST003 after about 6 x the acquisition timeoutmeasured, n = 2 per timeout (n = 1 at 4 s), repeated at 10 s in runs 1 and 2 and at 2 s in run 1client time per request; PostgREST access-log lines counted by User-Agent
A component in front resends after PGRST003inference; the public Envoy config fits, hosted config not checkedlog-line count and timing; the component and the competing reading were not separated
Bounds 0-1000 and 0-60documented, edges measured1001, -1 and 61 sent; 0 not sent
db_pool above max_connections gives PGRST002measured, one burst per arm220 concurrent 15 s RPCs at 50 and at 1000 on Large
Every PATCH restarts PostgRESTmeasured, 9 of 9 in run 4, a PATCH that answered 500 (DC04d), and 3 of 3 in DC08c (restart line after 1.8, 1 and 1.4 s)“Starting PostgREST” log lines against accepted PATCHes
Restart drains in-flight requestsinference from DC02d, twicein-flight outcomes, ping failures, restart line timing
Role timeouts and reloadmeasured, n = 2 per rowsleeping RPCs per key and role
Small ceiling 853 to 1,020 req/s, 4XL 3,158 from one Macmeasured, one step per cell, one indexed point read, one clientoha closed loop, 40 s per step
4XL with in-region clients gained 5 % at its top step, to 6,148 req/s (nothing above c=3072 run)measured, one run, one step per concurrency; the generator under-delivered, so labels overstate the loadEdge Function clients pinned to ap-northeast-2, 48 s steps, requests in flight derived as req/s x p50
No sign of the 4XL DC08 queue in Postgresmeasured, authenticator samples during load; Postgres rates as a floorManagement API reads of pg_stat_activity; pg_stat_statements deltas over a window longer than the load
db_pool 150 and 250 did not lift 4XL throughput or p50measured, one step per arm, no repeats; p95/p99 higher at 150 and 250DC08c at c=3072
Requests queued with free connections on 4XLone snapshot, n = 1, moment not knownDC08b pool tuple first scraped 33 s into c=3072
Pool 15 to 60 on Small: 853 to 909 req/s at 256measured, one step per cell; not separated from a 23 to 40 req/s repeat spreadDC07 against DC04b at pool 30
waiting = queued plus holding, available = open minus in usesource-read (v14.18), three snapshots agreeusePool and Metrics.hs; DC05a and DC05b against pg_stat_activity
Pool series are snapshots, refresh at least about 40 s apart if periodicmeasured, one 100 s window plus two PATCHes1 s scrapes against pg_stat_activity
Lock-induced timeouts leave no aborted connectionmeasured, one 12.6 s lock and a 2 s control, anon readsstatuses, 25P02 count, state samples about 5.7 s apart
#5336 throughput collapseupstream report, not reproducedthe issue’s own benchmark

Every figure comes from two vantages (one Mac in Singapore for DC01-DC07, Edge Function clients in the project’s region for DC08), two regions and a Pro organization on throwaway projects with no other traffic. The DC04 and DC07 throughput rows describe one request shape and one client machine, and DC08’s describe one mix from one generator; the pool sizes describe the sizes measured and say nothing about Nano, Micro, Medium, XL, 2XL, 8XL, 12XL or 16XL.


This is the page’s operational checklist; each row rests on a measured result unless the cell says otherwise.

PracticeEvidenceModule
With db_pool unset, count the pool mid-burst; GET /postgrest reads null.db_pool reads null while the pool was 30 on Small, 50 on Large, 70 on 4XL.DC01a, DC01d, DC04a, DC04c, DC08a, DC08b
Budget about 6 x db_pool_acquisition_timeout before a client sees PGRST003.Lone requests: 61364 / 61972 ms at 10 s, 13767 / 13752 ms at 2 s, 25007 ms at 4 s (n = 1); 6 log lines per request. Under a 220-request burst: 10923 to 20903 ms (DC02c).DC01b, DC02b, DC02c
Lower db_pool_acquisition_timeout to make a full pool fail faster.2 s gave 504 after 13767 and 13752 ms, against 61364 and 61972 ms at the default 10 s.DC02b
Count the resends a layer in front appears to make before retrying PGRST003.6 PostgREST log lines per client request; client retries were not run, so this row is design, not result.DC01b
Keep db_pool within what max_connections can give.1000 on 160 turned 70 x 504 PGRST003 into 66 x 503 PGRST002 in one burst per arm.DC02c
Treat every PATCH /postgrest as a PostgREST restart, a no-change PATCH included.9 start lines for 9 accepted PATCHes; a PATCH that answered 500 restarted it too; restart 0.5 to 1.5 s after with nothing long in flight.DC02a-DC02e, DC04d, DC05c, DC07
PATCH the PostgREST config when no long requests are in flight.With 60 s requests in flight: restart after 55.3 s, new requests unanswered for about 50 s, twice.DC02d
Wait for the restart line before measuring after a PATCH.Run 1 did not, and three of its rows look like restart overlaps.DC02 run 1
Run notify pgrst, 'reload config' after alter role ... set statement_timeout.8 s cancellations continued until the reload; then 10 s sleeps answered 200.DC03c
Put a long RPC’s set statement_timeout on the function.set statement_timeout = '30s' ran a 12 s sleep to 200 for anon, a user token and service_role.DC03b
Expect 8 s on the service_role key unless something raises it.rolconfig null; 500 57014 after 8056 and 8048 ms.DC03a
Test a larger compute before a larger db_pool for point-read throughput on Small.4XL with the default pool reached 3,158 req/s from one Mac; pools 15 to 60 on Small gave 853 to 909 at 256, not separated from repeat spread. DC08’s 6,148 on 4XL used a different mix and vantage and is not comparable to Small’s figures.DC04c, DC07
On 4XL, read authenticator states before raising db_pool for throughput.At c=3072, pools 150 and 250 gave p50 281 and 278 ms against 273 and 274 at 70, with 133 to 240 backends idle and at most 5 active; one step per arm; p95/p99 higher at 150 and 250. Busy backends and #5336’s pool of at least the requests in flight were not run, so the read-first step is design.DC08c
Load-test from in-region clients before reading a 4XL ceiling.One Mac stopped at 3,158 req/s (os error 51, 403s from an unidentified layer); in-region clients reached 6,148 and 6,210 req/s with a different mix. Vantage and mix both changed between the two runs, so this row is design.DC04c, DC08b, DC08c
Count answered clients and requests in flight when Edge Functions generate load.Invocations failed with 546, 503 and 429 and started up to 16.2 s late; at c=3072, 2,926 answered workers (365.7 x 8, derived) against about 1,680 requests in flight.DC08b
Read pgrst_db_pool_waiting minus (max - available) as a lower bound on the queue.waiting counts queued plus holding (source-read v14.18); 31 waiting, 1 of 15 available: at least 17 queued (DC07). Snapshots read 25 and 10 with nothing queued (DC05b).DC05b, DC07
Cross-check pgrst_db_pool_* against pg_stat_activity before acting on them.One change in 100 s of 1 s scrapes; no refresh in a 36 s window; a 4XL tuple of 1,078 waiting still showed 25 s after the load ended.DC05a, DC08b
Confirm a new pool via the restart line or GET /postgrest, not pgrst_db_pool_max.The metric showed the new value after 1.4 s once and 28.9 s once; restart line after 1.2 s and 1 s.DC02a, DC05c
Skip a pool restart after lock-induced 57014s on reads; recheck for writes.60 x 57014 during a 12.6 s lock, then 9920 x 200 and 0 x 25P02; one run per arm, anon reads, nothing queued for the pool, whole-instance stall not made.DC06

Data API requestsfail or slow down504 PGRST003-> pool full; check pg_stat_activity,shorten requests, lower the wait503 PGRST002 persisting after the restartfollowing a db_pool raise-> db_pool past max_connections503 PGRST002 in about 2.7 s whileanother client (Hyperdrive) holds the slots-> origin connection limits that fit500 57014 at 3 s / 8 s-> role timeout; function settingor alter role + reload configno answer just after a config PATCH-> PostgREST restartflat req/s, rising latency-> Small: compute size before db_pool-> 4XL, backends idle: db_pool 150 and 250did not help (one step each)

Text version:

  1. 504 PGRST003: every pooled connection is busy. Count authenticator backends in pg_stat_activity, shorten the requests holding them, or lower db_pool_acquisition_timeout to fail sooner.
  2. 503 PGRST002 that persists after the restart line following a db_pool raise: the pool is past what max_connections gives; bring it back down.
  3. 503 PGRST002 within about 2.7 s while another client such as Hyperdrive holds the connection slots: Postgres refused PostgREST a new connection, and PostgREST rebuilt its pool about 8 s later (hyperdrive-direct H03). Give the other client an origin connection limit that leaves room; see the connection budget.
  4. 500 57014 at about 3 s or 8 s: the role’s statement timeout. Set it on the function, or alter role and reload the config.
  5. No answer just after a PostgREST config PATCH: the restart; with long requests in flight it came when they finished (DC02d, twice).
  6. Flat req/s while latency rises with concurrency on Small: try a larger compute size before a larger db_pool. On 4XL at 6,148 req/s with most authenticator backends idle, db_pool 150 and 250 did not help (DC08c, one step each).

  • Compute sizes other than Small, Large and 4XL; regions other than ap-southeast-1 and ap-northeast-2; Team or Enterprise organizations.
  • db_pool 0 and db_pool_acquisition_timeout 0.
  • Which component resends after PGRST003, its policy under load, attempt counts inside a burst, and the x-envoy-attempt-count header on a PGRST003 answer.
  • The 60 s ceiling the timeouts guide gives for client queries.10
  • The pool metric refresh interval measured directly (only a lower bound if periodic), whether a restart refreshes the payload, and a waiting snapshot taken while requests were known to queue.
  • Whether the hosted PostgREST matches the v14.18 source read here.
  • A per-process CPU split between PostgREST, the API gateway and Postgres, the latency each layer between the client and PostgREST adds, and node CPU in DC07.
  • DC07 repeated per cell (DC08c ran 70, 150 and 250 on 4XL with a different generator, not DC07’s 15 to 60), on a size with more vCPU, or with requests that hold a connection longer than a point read, where waiting for a connection is certain.
  • Load above c=3072 on 4XL; a load generator whose delivered concurrency matches the requested one; clients outside the Edge Functions runtime (machines in the region) to compare against.
  • The pool series at a known refresh during DC08’s c=512 to c=2048 steps, and a pool reading during the 150 and 250 arms.
  • Issue #5336’s workaround on 4XL, a db_pool at least as large as the requests in flight (about 1,360 to 1,700 in DC08c, derived, req/s x p50).
  • Postgres rates over exactly the 48 s of DC08 load, and why calls and transactions per request fell at the top steps.
  • Requests with authenticated-user JWTs (every request used the anon key), larger payloads, writes with indexes or triggers, a mix without count=exact, and a high max_rows.
  • A whole-instance stall for DC06, a cancellation while queued for a pool connection, the authenticated role’s 8 s timeout under a lock, RPC or write requests under cancellation.
  • Requests through Supavisor or a direct Postgres connection; this page covers the Data API path only.
  • Client-side error text for the status-0 rows in run 4 and the 394 in DC08c’s pool-150 arm.
  • Repeats of any DC04-DC08 row beyond DC07’s pool-30 arm, which re-ran DC04b’s step on another project, and DC08c’s second pool-70 step; XL, 2XL and 8XL under load.

The experiment provisions and deletes its own project; it needs a Pro-plan organization slug in PVLAB_ORG_PRO and a personal access token in SUPABASE_ACCESS_TOKEN.

Terminal window
make run # DC01-DC03 then DC99, about 45 minutes
make run ONLY=DC04,DC99 # opt-in, needs oha; PVLAB_DC_REGION / PVLAB_DC_SIZE pick the project
make run ONLY=DC05,DC06,DC07,DC99
make run ONLY=DC08,DC99 # opt-in; PVLAB_DC_REGION, PVLAB_DC08_* pick size, steps, pools, workers, rounds
make clean # list leftover pvlab-dc-* projects; DELETE=1 deletes
make publish-evidence RUN=evidence/<ts>/run-<stamp>.json

Project time was about 40 minutes for run 4, 18 minutes for DC04, 21 minutes for DC05-DC07 and 22 minutes for DC08, each on a Small project except DC01d-DC03 on Large and the DC04c-DC04d tail and DC08 after its resize on 4XL. DC08 deploys its generator as an Edge Function on the project under test; the version that ran took its target host from the request, and the version in the lab now targets only its own project and has not been run.


ModuleExperimentTestArtifact
DC01data-api-capacitydc01-default-pool.tsrun 4 run-2026-10-23T00-37-06-485Z; runs 1 and 2 run-2026-10-22T23-02-55-371Z, run-2026-10-22T23-43-28-093Z
DC02data-api-capacitydc02-pool-config.tsrun 4 run-2026-10-23T00-37-06-485Z
DC03data-api-capacitydc03-statement-timeouts.tsrun 4 run-2026-10-23T00-37-06-485Z
DC04data-api-capacitydc04-throughput-stall.tsrun-2026-10-23T07-13-34-754Z
DC05data-api-capacitydc05-pool-metrics.tsrun-2026-10-23T07-49-11-858Z
DC06data-api-capacitydc06-aborted-after-stall.tsrun-2026-10-23T07-49-11-858Z
DC07data-api-capacitydc07-pool-size-under-waiters.tsrun-2026-10-23T07-49-11-858Z
DC08data-api-capacitydc08-inregion-mix.ts, generator gen/dc08-gen/index.ts (post-run version; the run’s version took the host from the request)run-2026-10-23T09-37-44-863Z
  1. Supabase, “Management API OpenAPI document (v1),” Supabase. https://api.supabase.com/api/v1-json ↩ ↩2 ↩3

  2. PostgREST, “Configuration: db-pool, db-pool-acquisition-timeout,” PostgREST 14 documentation. https://docs.postgrest.org/en/v14/references/configuration.html#db-pool-acquisition-timeout ↩ ↩2 ↩3 ↩4

  3. Supabase, “Compute and Disk,” Supabase Docs. https://supabase.com/docs/guides/platform/compute-and-disk ↩ ↩2 ↩3

  4. Supabase, “Connection management,” Supabase Docs. https://supabase.com/docs/guides/database/connection-management ↩ ↩2

  5. PostgREST, “Errors: Group 0 - Connection,” PostgREST 14 documentation. https://docs.postgrest.org/en/v14/references/errors.html#group-0-connection ↩ ↩2

  6. Supabase, ansible/files/envoy_config/lds.yaml (and lds.supabase.yaml), supabase/postgres at 920bb63. https://github.com/supabase/postgres/blob/920bb634698fef8dee6e78d738330d2dd8e489c5/ansible/files/envoy_config/lds.yaml#L421-L427 ↩

  7. Envoy, “Router: x-envoy-retry-on, x-envoy-attempt-count,” Envoy documentation. https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter ↩

  8. Supabase, ansible/files/envoy_config/cds.yaml, postgrest cluster, supabase/postgres at 920bb63. https://github.com/supabase/postgres/blob/920bb634698fef8dee6e78d738330d2dd8e489c5/ansible/files/envoy_config/cds.yaml#L45-L64 ↩

  9. Envoy, “Circuit breakers (proto): RetryBudget,” Envoy documentation. https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto ↩

  10. Supabase, “Timeouts,” Supabase Docs. https://supabase.com/docs/guides/database/postgres/timeouts ↩ ↩2 ↩3

  11. Supabase, ansible/tasks/setup-postgrest.yml, supabase/postgres at 920bb63. https://github.com/supabase/postgres/blob/920bb634698fef8dee6e78d738330d2dd8e489c5/ansible/tasks/setup-postgrest.yml ↩

  12. PostgREST, “Pagination and Count: Exact Count,” PostgREST 14 documentation. https://docs.postgrest.org/en/v14/references/api/pagination_count.html#exact-count ↩

  13. PostgREST GitHub Issue #5336, “Throughput collapses when requests wait for a pool connection: CPU per request grows 25x, and more cores make it worse,” opened 2026-10-16. https://github.com/PostgREST/postgrest/issues/5336 ↩

  14. PostgREST, postgrest.cabal, hasql-pool dependency, tag v14.18. https://github.com/PostgREST/postgrest/blob/v14.18/postgrest.cabal#L118 ↩

  15. PostgREST, “Observability: Connection Pool Metrics,” PostgREST 14 documentation. https://docs.postgrest.org/en/v14/references/observability.html#connection-pool-metrics ↩ ↩2 ↩3

  16. PostgREST, src/PostgREST/AppState.hs, usePool, lines 161-167, tag v14.18. https://github.com/PostgREST/postgrest/blob/v14.18/src/PostgREST/AppState.hs#L161-L167 ↩

  17. PostgREST, src/PostgREST/AppState.hs, usePool callers at lines 275, 319, 348, 392 and 402, tag v14.18. https://github.com/PostgREST/postgrest/blob/v14.18/src/PostgREST/AppState.hs#L275 ↩

  18. PostgREST, src/PostgREST/Metrics.hs, calcAvailable, lines 59-61, tag v14.18. https://github.com/PostgREST/postgrest/blob/v14.18/src/PostgREST/Metrics.hs#L59-L61 ↩