Connection rationing  / field guide
Practitioner field guide · 2026-09-06

A million clients, four hundred connections

Every relational database serves far more clients than it can hold sessions for, so something between the application and the database must ration connections. This guide reconstructs how that ration line is drawn in production at GitLab, Notion, Figma, OpenAI, Cloudflare and others, what it costs, and the four ways it fails.

34 primary sources 12 production systems 5 published incidents Evidence through September 2026 Read: 25 min
01

The territory

The problem, who has solved it in production, and the finding that reorganises how you should think about it.

>2×
Slowdown of a single active connection when many idle connections exist, measured on Postgres 13
2,500:1
Client sessions per server connection in Supabase's benchmark: 1,000,000 clients onto 400 connections
46%
Drop in read-only throughput from 1,000 idle connections on a small RDS instance
15–250%
Query throughput recovered when the pooler learned to track prepared statements

State the problem without naming a product. A relational database executes work on a small, fixed number of operating system processes, and each client that might issue a query wants a standing, stateful session held open on its behalf. Demand for sessions grows with the size of the application fleet, and with serverless it grows with request volume itself, while the supply of sessions the engine can hold barely grows at all. The largest hosted Postgres plans topped out around 500 connections when Brandur Leach surveyed them in 2018, and the picture inside the engine explains why: Andres Freund's 2020 measurements found that a single active connection slows by more than 2x in the presence of many idle ones, because every transaction rebuilds a snapshot that, before Postgres 14, touched shared state for every open connection. Meanwhile Supabase demonstrated one million connected clients served by 400 actual database connections. The gap between those two numbers, three to four orders of magnitude, is the territory of this guide.

The teams that have written about crossing this gap in production include GitLab, Notion, Figma, OpenAI, Cloudflare, ClickHouse and Supabase; the published failures come from Red Hat's Quay.io, GitHub, Honeycomb and GitLab again. Every one of them converges on the same two-line architecture drawn in Figure 1, and every one of them has hit at least one of the four failure classes in section 4.

The finding that surprised me, and that reorganises the topic: the hard part stopped being connection reuse years ago. Reuse is a solved mechanism you can install in an afternoon. What the most mature deployments are actually engineering at the pool is admission control, the decision of who waits, in what order, and who is refused when the budget is short. Figma's 2026 account is the clearest statement: they replaced the standard pooler with PGKeeper, a service whose headline features are queue scheduling (CoDel plus adaptive LIFO), token-bucket limits on connection churn, and weighted fair sharing across traffic classes, and they report it prevented more than 20 would-be incidents in a single quarter. The pool, it turns out, is where load management lives, because it is the one chokepoint every query already passes through. The theory agrees: a bounded pool converts an open arrival process into a partly closed one, which is precisely the regime that Schroeder, Wierman and Harchol-Balter showed behaves qualitatively differently under overload.

Scope. This guide covers the connection path between an application fleet and a single-writer relational database: pool sizing, pooler topology, pooling modes and their semantics, and the published failure record, with Postgres as the best-documented case and MySQL appearing through GitHub's ProxySQL and Quay.io's incidents. It deliberately does not cover read-replica routing policy, sharding (a separate guide in this series covers resharding), driver-internal retry semantics, or serverless database engines that dissolve the connection limit inside the service, such as the multiplexing described in the Aurora Serverless paper, which appears here only as a boundary marker.

Figure 1 · Where the ration line is drawn

Ration line 2 · shared tier

Ration line 1 · in-process

Demand side

cannot hold a pool
across invocations

tens per database

Web and worker fleets
Figma, GitLab, Notion, OpenAI

Serverless functions
Lambda, Workers

App-side pool
a few dozen connections

Pooler in transaction mode
PgBouncer, Supavisor,
PGKeeper, RDS Proxy, Hyperdrive

Database
low hundreds of
server connections

Ration line 2 · shared tier

Ration line 1 · in-process

Demand side

cannot hold a pool
across invocations

tens per database

Web and worker fleets
Figma, GitLab, Notion, OpenAI

Serverless functions
Lambda, Workers

App-side pool
a few dozen connections

Pooler in transaction mode
PgBouncer, Supavisor,
PGKeeper, RDS Proxy, Hyperdrive

Database
low hundreds of
server connections

Two ration lines recur in every published system: a small in-process pool, and a shared pooler tier in front of the database. Serverless clients cannot hold the first line, which is why they stress the second. Sources: GitLab runbook, Notion, 2023, Cloudflare, 2025.
Diagram source
02

How it is actually built

The common shape across GitLab, Notion, OpenAI, Figma and Supabase, with the points where they diverge and why.

The reference architecture has three tiers of scarcity, and each published system draws them the same way. At the top, every application process keeps a private pool measured in single digits to low tens; the HikariCP project's long-standing guidance is "you want a small pool, saturated with threads waiting for connections", sized near cores × 2 + spindles for the database host, and its most-cited exhibit is an Oracle demonstration in which shrinking the pool, with no other change, cut application response times from roughly 100 ms to roughly 2 ms. In the middle sits a shared pooler tier speaking the database's wire protocol, run in transaction mode so a server connection is lent out only for the duration of one transaction. At the bottom, the database itself is given a deliberately small ceiling: Notion capped each Postgres instance at 200 connections during its 2023 re-shard, and OpenAI fronts every one of its roughly 50 read replicas with PgBouncer while pushing more than a million queries per second through the fleet.

Why transaction mode wins the middle tier is stated plainly in GitLab's runbook: their clients "use long-lived connections to execute transactions from different requests spread over time", so a session-mode pooler would hold a server connection hostage to every idle client. Transaction mode turns idle clients from a cost on the database into a cost on the pooler, and poolers are far better at holding idle sockets: Supavisor's repository documents 250,000 idle client connections on a single 16-core node, where the same quarter-million connections held open on Postgres would consume, at Freund's measured 1.3 to 7.6 MiB each, somewhere between 300 GiB and 1.8 TiB of memory and would degrade every active query through snapshot construction besides. That arithmetic is mine; the per-connection figures are Freund's 2020 measurements.

The divergence points are three. First, placement: GitLab runs three dedicated pooler hosts in front of the primary but co-locates three pooler processes on each replica host, a split that follows write traffic's need for independent failover. Cloudflare moved the pool off the customer's premises entirely; Hyperdrive holds pools near the database and lets edge clients skip the seven round trips a fresh connection costs (one TCP, three TLS, three auth). Second, process model: PgBouncer is single-threaded by design, so GitLab, ClickHouse and Crunchy Data all scale it by running several processes behind SO_REUSEPORT; ClickHouse measured one process peaking at about 87k transactions per second and a per-core fleet reaching about 336k. Supabase and Figma instead replaced the pooler with a multi-threaded one. Third, what the pooler is for: at Figma the replacement, PGKeeper, is explicitly a load-management service, with pool warming, token-bucket limits on connection creation and teardown, and two-stage admission control; connection reuse is almost incidental.

One mechanism deserves its own paragraph because it explains a decade of production errors: protocol-level prepared statements. A prepared statement lives in one server session. Under transaction pooling, consecutive transactions from the same client land on different server connections, so the statement a client prepared is marooned on a connection it may never see again. For years the only fix was to disable server-side preparation entirely and forfeit its performance; PgBouncer 1.21 (October 2023) finally tracks prepared statements per client and re-prepares them on whatever server connection the transaction lands on, and Cloudflare's Hyperdrive implements the same per-client, per-origin-connection bookkeeping. The release notes put the recovered throughput at 15 to 250 percent depending on workload.

Figure 2 · Reference architecture, with divergence points

Database hosts

Pooler tier · transaction mode

Application fleet · N processes

default_pool_size
tens, not hundreds

reads

reads

Process + pool
5-10 connections

Process + pool
5-10 connections

Pooler process 1

Pooler process 2
SO_REUSEPORT fleet

Primary
capped near 200
connections

Each replica
+ its own local pooler

Database hosts

Pooler tier · transaction mode

Application fleet · N processes

default_pool_size
tens, not hundreds

reads

reads

Process + pool
5-10 connections

Process + pool
5-10 connections

Pooler process 1

Pooler process 2
SO_REUSEPORT fleet

Primary
capped near 200
connections

Each replica
+ its own local pooler

Every box is attested: pool-per-process at HikariCP's guidance, the transaction-mode tier and its process fleet at GitLab and ClickHouse, the 200-connection instance cap at Notion, the pooler-per-replica pattern at OpenAI.
Diagram source

In-process pool

Holds a handful of connections per application process and queues threads that want one. Its size is set by the database host's parallelism, not by client count.

Doctrine: HikariCP wiki. In production at effectively every JVM, Rails and Go shop.

Transaction-mode pooler

Lends a server connection for one transaction at a time, so thousands of idle clients cost the database nothing. The price is session semantics; see section 3.

Runs this way at GitLab, Notion, OpenAI.

Server connection budget

A deliberate cap in the low hundreds per instance, because idle server connections tax memory and, pre-Postgres 14, every snapshot. The budget is the scarce resource the whole stack rations.

Measured cost: Freund, 2020; cap: Notion, 2023.

Admission control

Queue scheduling, rate limits on connection churn, and fair sharing across traffic classes, placed at the pool because every query already passes through it.

The built case: Figma's PGKeeper, 2026.

Pause/resume control plane

Because every client goes through it, the pooler doubles as the switch for zero-downtime failover: pause traffic, drain in-flight queries, repoint, resume.

Used for a 32-to-96-instance migration at Notion, 2023.

Saturation telemetry

Client connections waiting, server connections active, and checkout wait time are the three signals; GitLab alerts on pooler client-connection saturation as a first-class incident trigger.

Incident opened from that signal: GitLab, 2022.

03

The decisions that matter

Each fork as the published record shows it: what was chosen, what was rejected, the stated reason, and the condition that flips the answer.

Do we need a shared pooler tier at all, or do in-process pools suffice?

Chosen
  • In-process pools only, while fleet × pool size stays well under the server cap
  • Figma ran this way until connections were "in the thousands" in 2020, then inserted PgBouncer
Rejected
  • A pooler tier from day one
  • It adds a hop, an operational component, and the session-semantics tax before the scale that justifies them
Flips when
  • Instance count × per-instance pool approaches the database cap, or any serverless client appears; Quay.io's outage shows what the default (no per-worker cap) does at fleet scale

Session pooling or transaction pooling?

Chosen
  • Transaction mode, at GitLab, Notion, OpenAI and Hyperdrive
  • Long-lived, mostly idle client connections would pin a server each in session mode
Rejected
  • Session mode as the default
  • It only relocates the connection limit; the multiplexing ratio stays 1:1
Flips when
  • The workload depends on session state: advisory locks, LISTEN/NOTIFY, temp tables, session-level SET. The pooler docs say "clients must not use any session-based features" in transaction mode, and JP Camara's casualty list shows what happens when they do

The pooler's core is pegged. Fleet of processes, or a different pooler?

Chosen
Rejected
  • Multi-threading PgBouncer itself: proposed in issue #1021, open since February 2024
  • The maintainer's own issue lists the process fleet's costs: pool limits not shared, per-process stats, awkward setup
Flips when
  • Un-shared pool limits and per-process observability become operationally untenable, or you need zero-downtime scaling of the pooler itself; that is the stated motivation for Supavisor and part of Figma's case for PGKeeper

Prepared statements under transaction pooling: disable them, or track them?

Chosen (2023 onward)
Rejected (for a decade)
  • The status quo: disable preparation (prepareThreshold=0 and equivalents), documented in driver issues back to 2017
  • A parallel implementation, PR #757, was closed unmerged in favour of #845 after a year of review found protocol and hashing defects
Flips when
  • You run a pooler version below 1.21 or one that never gained tracking; then disabling preparation is still the only safe configuration, and you are paying the 15 to 250 percent throughput tax the release notes quantify

One decision deserves narrative rather than a table: whether to solve the problem inside the database. Raising max_connections is the intuitive move and every measured source argues against it. Freund's analysis concluded the binding constraint was snapshot scalability, not memory, and his own Postgres 14 commits attacked exactly that, calling the old snapshot construction "the most significant source of GetSnapshotData() scaling poorly on larger systems". Postgres 14 raised the ceiling; it did not remove it. The engine still spends real memory and scheduling capacity per connection, which is why the AWS idle-connection measurements below remain relevant six years on, and why every large deployment in this corpus still rations. Nobody in the published record scaled out of this problem with a configuration change.

Figure 3 · The decision path, condensed

no

yes

yes

no

no

yes

yes

no

Fleet x pool size near
the server connection cap?

Keep in-process pools,
sized cores x 2 + spindles

Session features required?
advisory locks, LISTEN,
temp tables, session SET

Session pooling for those
clients, or remove the
session dependence first

Pooler core pegged?

One transaction-mode pooler,
prepared statement tracking on

Process-fleet costs
acceptable? shared limits,
per-process stats

SO_REUSEPORT fleet
GitLab, ClickHouse

Multi-threaded pooler or build:
Supavisor, pgcat, PGKeeper

no

yes

yes

no

no

yes

yes

no

Fleet x pool size near
the server connection cap?

Keep in-process pools,
sized cores x 2 + spindles

Session features required?
advisory locks, LISTEN,
temp tables, session SET

Session pooling for those
clients, or remove the
session dependence first

Pooler core pegged?

One transaction-mode pooler,
prepared statement tracking on

Process-fleet costs
acceptable? shared limits,
per-process stats

SO_REUSEPORT fleet
GitLab, ClickHouse

Multi-threaded pooler or build:
Supavisor, pgcat, PGKeeper

Terminal nodes are actions. The left exits are the common cases; the bottom right is where GitLab sits deciding today and where Supabase and Figma already went. Derived from the decision blocks above and their sources.
Diagram source
DecisionChosenRejectedBecauseEvidence
Pooler tierInsert when connections hit thousandsDay-one poolerCost precedes benefit at small scaleFigma, 2023
Pool modeTransactionSessionIdle clients must not pin serversGitLab runbook
Pooler scalingSO_REUSEPORT process fleetMulti-threading the poolerThreading remains an open issue since 2024pgbouncer #1021
Prepared statementsPooler-side tracking (1.21+)Disable preparation15–250% throughput at stakeRelease 1.21.0
Raise max_connectionsRation and queue insteadBig connection capsIdle connections tax every active queryFreund, 2020
Load managementAdmission control at the poolPer-service client throttles aloneThe pool sees all traffic and the true budgetFigma, 2026
04

What broke in production

Five published incidents and one chronic failure, grouped into the four classes the record actually contains.

Group the incidents and four classes emerge. Storms: unbounded clients open connections faster than the database can absorb them. Recovery herds: the outage ends, and simultaneous reconnection and cold caches re-create it. Semantic leaks: transaction pooling silently changes session semantics and the application finds out in production. Pooler saturation: the ration line itself becomes the bottleneck. The first two are capacity failures with an open-loop signature; Marc Brooker's summary of the queueing theory applies to both: congestive collapse is the characteristic risk of open-loop systems, and retries add arrival traffic exactly when completions slow.

Figure 4 · The storm-and-herd loop

DatabasePool tierApp fleetDatabasePool tierApp fleetpool exhausted,waiters queueCPU goes to handshakes andsession setup, queries slow furtherloop sustains itself until trafficis refused at the front doorcheckoutquery, slightly slow tonightcheckouts pile up behind itcheckout timeoutretries, restarts, new connectionsreconnect storm, authhandshakescrashed workers return with coldcachesextra read load on top
DatabasePool tierApp fleetDatabasePool tierApp fleetpool exhausted,waiters queueCPU goes to handshakes andsession setup, queries slow furtherloop sustains itself until trafficis refused at the front doorcheckoutquery, slightly slow tonightcheckouts pile up behind itcheckout timeoutretries, restarts, new connectionsreconnect storm, authhandshakescrashed workers return with coldcachesextra read load on top
The sequence Quay.io, GitHub and Honeycomb each describe a segment of: a small slowdown converts waiting clients into new connections, and connection setup work then sustains the slowdown. Sources: Red Hat, 2020, Honeycomb, 2019.
Diagram source
Postmortem

Quay.io: the defaults were the outage

AssumptionPer-worker database connection behaviour could be left at library defaults.
What happenedLoad produced "a storm of tens of thousands of database connections, effectively locking the MySQL instance". Each gevent worker opened connections as it pleased; the fleet multiplied that freedom.
Blast radiusRegistry unavailable; repeated recurrence during recovery attempts (2020).
FixA configurable per-worker connection cap, staged load tests that located the real ceiling near 10,000 connections, more caching, a bigger instance.
Design ruleEvery connection budget must be explicit at every tier. A tier without a cap has one anyway; you find it during an incident.
Postmortem

GitHub: the threshold nobody had metered

AssumptionThe new pooling tier would degrade proportionally under rising load.
What happened"Active database connections crossed a critical threshold that changed the behavior of this new infrastructure": ProxySQL, the pooling layer for the busiest MySQL cluster, stopped serving queries consistently once past it.
Blast radiusMultiple service disruptions across February 2020; the third incident in a row implicating the same tier.
FixPost-incident analysis and re-architecture of the connection-handling thresholds; published as a dedicated analysis.
Design ruleA pooler has cliffs, not slopes. Find the threshold in a load test and alert well below it, because behaviour past it is a different regime, not a slower one.
Postmortem

Honeycomb: recovery was the second incident

AssumptionOnce the trigger cleared, bringing services back would restore steady state.
What happenedTotal outage, 2023-07-25, 13:40 to 14:48 UTC. The team found that "bringing ingest back without a cache would make it go down again, either through overload or database connection saturation"; the database host hard-locked and required failover to a replica.
Blast radiusAll user-facing services, 68 minutes.
FixDeliberate circuit-breaking: refuse ingest traffic with 5xx at the front while caches warmed and the connection budget recovered.
Design rulePlan the reconnection herd as part of the failure. Recovery needs an admission ramp, and the pool is where you meter it.
Postmortem

Honeycomb: a 10% slowdown became 100% concurrency

AssumptionCache refresh under load would stay proportional to traffic.
What happenedRDS slowed slightly; expired cache entries sent goroutines to the database in parallel, each holding a connection longer; crashes cleared caches and re-hit the database. "RDS was stalled at ~90% CPU" while the feedback loop ran (incidents of Oct 4 and Oct 11, 2019).
Blast radiusPartial API outages of 54 and 62 minutes.
FixSingle-flight cache refresh: one goroutine refreshes, everyone else reads the stale value.
Design ruleConcurrency toward the database must be bounded above by design, not by incident. Request coalescing is connection rationing by another name.
Postmortem

GitLab: the ration line itself saturated

AssumptionThe pooler tier had headroom over the application fleet's growth.
What happenedClient connections to the primary's PgBouncer pool hit their ceiling; incident declared 2022-08-08 12:16 UTC. The background design issue records the mechanism: latency degraded "when we scaled up the web worker fleet and/or bumped pgbouncer's max_client_conn", because the single-threaded pooler was "pegging one core".
Blast radiusElevated errors and latency on GitLab.com's primary database path (the public review's impact fields were left incomplete).
FixMore pooler processes and, longer-term, the design work tracked in the scalable-pooling issue.
Design ruleCapacity-plan the pooler like a database: one core per process, roughly a thousand active clients per process, and a saturation alert on client connections, not just server ones.
Chronic

The decade of "S_1 does not exist"

AssumptionA pooled connection behaves like a session, because it always had.
What happenedUnder transaction pooling, session state maroons on whichever server connection ran it. JDBC users hit prepared statement "S_1" does not exist even with preparation nominally disabled, from 2017 onward; session-level SET statement_timeout leaks to other clients' transactions. JP Camara's 2023 survey concludes "the road to downtime is paved with session level statements".
Blast radiusNot one incident but a class: intermittent errors and cross-tenant state leaks that surface only under specific interleavings, which load tests rarely produce.
FixPooler-side tracking for prepared statements (PgBouncer 1.21, 2023); for everything else, an audit of session-feature use before switching modes. RDS Proxy's answer is pinning, which silently degrades multiplexing instead of erroring.
Design ruleChanging pool mode is an application-semantics migration, not an infrastructure toggle. Inventory session state first; the errors you skip this step to avoid will find you at peak.

Figure 5 · How session state maroons under transaction pooling

Client BServer conn 2Server conn 1Pooler, transactionmodeClient AClient BServer conn 2Server conn 1Pooler, transactionmodeClient AA's transaction ends,conn 1 returns to the poolB silently inherits A'stimeout and session stateSET statement_timeout,PREPARE s1applied on conn 1 onlynext transaction, EXECUTE s1routed to conn 2ERROR: prepared statement does not existordinary transactiondraws conn 1
Client BServer conn 2Server conn 1Pooler, transactionmodeClient AClient BServer conn 2Server conn 1Pooler, transactionmodeClient AA's transaction ends,conn 1 returns to the poolB silently inherits A'stimeout and session stateSET statement_timeout,PREPARE s1applied on conn 1 onlynext transaction, EXECUTE s1routed to conn 2ERROR: prepared statement does not existordinary transactiondraws conn 1
Client A's SET and prepared statement live on server connection 1; A's next transaction lands on connection 2, and whoever draws connection 1 inherits A's state. Reconstructed from the pooler's own documentation and Camara's worked examples.
Diagram source
05

Numbers you can plan against

Everything quantitative in the corpus, with where it was measured and when. Treat the ratios as durable and the absolutes as dated.

MetricValueAtContextAs ofSource
Idle-connection tax on one active query>2× slowerPostgres 1310,000 idle connections, measured pre-snapshot-fix2020Freund
True memory per idle connection1.3–7.6 MiBPostgresProportional set size; 1.3 with huge_pages, not the folkloric tens of MB2020Freund
Throughput loss from 1,000 idle connections−8.7% / −46%AWS RDSdb.m5.large (2 vCPU, 8 GB); mixed load / select-only2020AWS
Response time from shrinking the pool alone~100 ms → ~2 msOracle demoNo other change; the small-pool doctrine's exhibitn.d.HikariCP wiki
One pooler process, connection ceiling~10,000 / ~1,000 activeCrunchy DataPlanning guidance for single-threaded PgBouncer2022Crunchy
One pooler process, throughput ceiling~87k tpsClickHouseDegrades to 77k at 256 clients; single core saturated2026ClickHouse
SO_REUSEPORT fleet, same host~336k tps (~4×)ClickHouseMultiple PgBouncer processes, one per core2026ClickHouse
Idle clients per purpose-built pooler node250,000Supabase16 cores, 64 GB; Elixir, multi-threaded2023Supavisor README
Multiplexing ratio at benchmark extreme1,000,000 : 400Supabase20k QPS through 400 server connections, 2 pooler nodes2023Supabase
Hosted Postgres connection caps500 / 20–25Heroku et al.Largest plans / smallest plans, at survey time2018Leach
Per-instance server connection budget200NotionDeliberate cap during 96-instance re-shard; 8 conns per pooler per shard2023Notion
Largest published single-primary deployment~50 replicas, >1M QPSOpenAIPgBouncer per replica; connection time 50 ms → 5 ms2026OpenAI
Round trips to a fresh connection7Cloudflare1 TCP + 3 TLS + 3 auth, before the first query2025Cloudflare
Prepared statements, throughput recovered+15–250%PgBouncerWorkload-dependent; feature landed October 20232023Release notes
Incidents prevented by pool-level admission control>20 in one quarterFigmaCompany-reported count for PGKeeper, Q4 20252026Figma
Measured, claimed, derived, unknown

Measured: the Freund, AWS, ClickHouse and Supabase rows come from described benchmark setups. Claimed: Figma's incidents-prevented count and OpenAI's QPS are company-reported without a public method; treat them as claims from credible primaries. Derived: the 300 GiB to 1.8 TiB figure in section 2 is my arithmetic (250,000 × 1.3 to 7.6 MiB) and is labelled as such where it appears. Unknown: nobody in this corpus publishes the money cost of a pooler tier (instances, cross-AZ traffic, licensing for managed proxies), so any cost model you build will rest on your own cloud bill, not on public evidence. The dominant variables, from the architecture: pooler cores (one per ~87k tps), pooler memory (idle clients), and one extra network hop on every query.

06

The evidence wall

Every source behind this page, graded. The ledger shipped beside this file (sources.md) carries the exact supporting quotes and the retrieval method for each.

Postmortem Red Hat / Quay.io2020

About the Quay.io Outage: Post Mortem

A fleet of workers on default connection settings produced tens of thousands of connections and locked MySQL; load tests later located the real ceiling near 10,000.

Carry forwardUncapped tiers have a cap anyway; find it in a load test, not an outage.
redhat.com
Postmortem GitHub2020-03

February service disruptions post-incident analysis

Three incidents in a month traced to the pooling tier; the third began when active connections "crossed a critical threshold that changed the behavior" of ProxySQL.

Carry forwardPoolers fail as regime changes at thresholds, not as gradual slowdowns.
github.blog
Postmortem Honeycomb2023-08

Incident Review: What Comes Up Must First Go Down

A 68-minute total outage whose recovery had to be engineered: ingest was refused with 5xx to protect the database connection budget while caches warmed.

Carry forwardDesign the reconnection herd's admission ramp before you need it.
honeycomb.io
Postmortem Honeycomb2019

RDS Clogs & Cache-Refresh Crash Loops

A modest database slowdown multiplied concurrent cache refreshes, each holding a connection; crash loops cleared caches and re-hit the database at 90% CPU.

Carry forwardBound concurrency toward the database by design; single-flight the refreshes.
honeycomb.io
Postmortem GitLab2022-08

pgbouncer_client_conn_primary saturation

The pooler's own client-connection budget saturated on the primary path. The public review's fields were left incomplete; the incident record and its saturation metric are the evidence.

Carry forwardAlert on the pooler's client-side saturation, not only server connections.
gitlab.com
Decision record GitLabn.d.

More scalable database connection pooling

The design problem stated by the operator: latency degrades when the web fleet grows or max_client_conn rises, because the single-threaded pooler pegs one core.

Carry forwardThe pooler is a capacity-planned component with its own scaling roadmap.
gitlab.com
Decision record PgBouncer2024-02

Issue #1021: multi-threading in PgBouncer

The maintainer's own case for threading, recording exactly what the SO_REUSEPORT workaround costs: un-shared pool limits, per-process statistics, awkward setup. Open as of September 2026.

Carry forwardThe process-fleet workaround's costs are documented; weigh them before adopting it.
github.com
Source HikariCPn.d.

About Pool Sizing (project wiki)

The small-pool doctrine and its formula, with the Oracle demonstration of a 50x response-time improvement from shrinking the pool alone.

Carry forwardSize pools by the database host's parallelism; queue the rest.
github.com
Source PostgreSQL2020-08

Commit dc7420c2c92: snapshot scalability

The commit message names the mechanism of the connection tax: snapshot construction caused "many cache misses" and was "the most significant source" of poor scaling on large systems. Postgres 14 deferred that work.

Carry forwardThe connection ceiling is version-dependent; retest assumptions on every major upgrade.
github.com
Source PgBouncer2023-10

PR #845 and release 1.21.0

The merged prepared-statement tracking, called "probably one of the most requested features" in the project's history, worth 15 to 250 percent throughput.

Carry forwardOn 1.21+, set max_prepared_statements and stop disabling preparation.
github.com
Source PgBouncer2022–2023

PR #757, closed unmerged

The parallel prepared-statement implementation, closed after a year when review surfaced hash-collision and protocol defects; the recorded argument about why this feature is hard.

Carry forwardProtocol-level state tracking in a proxy is subtle; prefer the implementation that survived review.
github.com
Source pgjdbc2017-07

Issue #869: preparation you cannot turn off

With prepareThreshold=0, autoCommit=false and a fetchSize, the JDBC driver still created a named statement, and transaction pooling produced "S_1 does not exist".

Carry forwardDriver behaviour, not application code, decides whether you use session state.
github.com
Source GitLabcurrent

PgBouncer runbook

The deployed topology: three dedicated pooler hosts before the primary, three co-located processes per replica, transaction mode justified by long-lived idle client connections.

Carry forwardWrite pooling and read pooling deserve different placement and failover.
gitlab.com
Source Supabasecurrent

supavisor repository

A clustered, multi-threaded pooler built because the standard one "is single-threaded, making it difficult to scale"; documents 250k idle connections per 16-core node and zero-downtime scaling as a goal.

Carry forwardIdle client connections are cheap at a pooler built to hold them.
github.com
Case study OpenAI2026

Scaling PostgreSQL to power 800 million ChatGPT users

One primary, roughly 50 replicas, a pooler in front of each, more than a million queries per second; connection time fell from 50 ms to 5 ms behind the pooler.

Carry forwardThe pooler-per-replica pattern holds at the largest published scale.
openai.com
Case study Supabase2023

Supavisor: scaling Postgres to 1 million connections

A described benchmark: one million client connections across two pooler nodes, triaged into 400 database connections at 20k QPS.

Carry forwardThe client-to-server ratio can be three orders of magnitude when idle clients dominate.
supabase.com
Eng blog Microsoft / Citus (Freund)2020-10

Analyzing the limits of connection scalability in Postgres

The measurements behind this whole topic: the 2x idle tax, the memory myth (1.3 to 7.6 MiB, not tens), and the identification of snapshots as the binding constraint.

Carry forwardIdle is not free; budget connections like RAM.
citusdata.com
Eng blog Figma2023

The growing pains of database architecture

The canonical first move, dated: pooler inserted when app connections reached the thousands, alongside replicas and vertical partitioning, under 3x annual traffic growth.

Carry forwardThe pooler buys time; it does not remove the need for the next move.
figma.com
Eng blog Figma2026

PGKeeper: building the bouncer we needed for Postgres

The endgame account: outgrew the standard pooler on threading, load management and extensibility; built a Go service with CoDel plus adaptive-LIFO admission control and connection-churn rate limits; reports more than 20 incidents prevented in Q4 2025.

Carry forwardAt the top end, the pool is an admission controller that happens to reuse connections.
figma.com
Eng blog Notion2023-07

The Great Re-shard

Explicit budgets everywhere: 96 instances at 200 connections each, four pooler clusters of 24 databases, eight connections per pooler per shard, and pause/resume as the zero-downtime failover mechanism.

Carry forwardThe pooler is the control plane for topology changes, not just a multiplexer.
notion.com
Eng blog Cloudflare2025-04

Pools across the sea

The connection-setup arithmetic (seven round trips) and a transaction-mode pooler at the edge with per-client, per-origin prepared-statement tracking.

Carry forwardPlace the pool where the round trips are cheapest, near the database.
cloudflare.com
Eng blog ClickHouse2026

How we scale PgBouncer in managed Postgres

Measured single-process ceiling (~87k tps, degrading past saturation) and the SO_REUSEPORT fleet's ~4x recovery, with the one-core-of-sixteen framing.

Carry forwardOne pooler process per core is the planning unit, not one per host.
clickhouse.com
Eng blog Crunchy Data2022

Postgres at scale: running multiple PgBouncers

The operator's planning numbers: roughly 10,000 connections per process, roughly 1,000 concurrently active, one core forever.

Carry forwardCPU at 100% on the pooler is the scale-out signal; watch it explicitly.
crunchydata.com
Eng blog Brandur Leach2018-10

How to manage connections efficiently in Postgres

The hosted ceilings (500 at the top, 20 to 25 at the bottom) and the practitioner techniques: minimum viable checkout time, releasing connections around slow external calls.

Carry forwardHold a connection only while touching the database; release it around foreign work.
brandur.org
Eng blog JP Camara2023-04

PgBouncer is useful, important, and fraught with peril

Worked examples of transaction-mode semantic leaks: statement_timeout crossing clients, advisory locks, driver quirks. "The road to downtime is paved with session level statements."

Carry forwardAudit every SET, lock and LISTEN before switching pool modes.
jpcamara.com
Eng blog Marc Brooker2023-05

Open and Closed, Omission and Collapse

The queueing frame applied by a practitioner: closed loops queue and stay stable, open loops collapse under overload, and retries add arrivals precisely when service slows.

Carry forwardA bounded pool is a stability mechanism; removing the bound removes the stability.
brooker.co.za
Paper Schroeder, Wierman, Harchol-Balter2006-05

Open Versus Closed: A Cautionary Tale (NSDI '06)

The formal result underneath this guide: open and closed workload models diverge vastly under load, most designers ignore the distinction, and eight principles govern the difference.

Carry forwardLoad-test with an open model or you will never see the storm your pool exists to stop.
usenix.org
Paper AWS (Barnhart, Brooker et al.)2024

Resource Management in Aurora Serverless (VLDB 17.12)

The boundary marker: inside a managed serverless engine, connection and buffer capacity is oversubscribed and reactively managed against a measured definition of "heat", dissolving the fixed budget this guide is about.

Carry forwardPaying a vendor to ration for you moves the problem; it does not delete the queueing.
amazon.science
Talk Jelte Fennema-Nio (Microsoft)2024-06

Comparing Postgres connection pooler support for prepared statements

By the engineer who landed the PgBouncer implementation: how PgBouncer, Odyssey, pgcat and Supavisor differ in prepared-statement design and performance. Claim taken from the published abstract; this build environment could not retrieve timestamps.

Carry forwardPrepared-statement support is a pooler selection criterion, not a given.
youtube.com
Talk Bohan Zhang (OpenAI)2025-06

Scaling Postgres to the next level at OpenAI (POSETTE 2025)

The conference companion to the OpenAI case study: pooler management, replica scaling and long-query handling at millions of QPS. Claim taken from the published session description.

Carry forwardConnection management is a named chapter in the largest deployment's own story.
posetteconf.com
Vendor PgBouncer docscurrent

config.md: pool modes and defaults

The semantics in the operator's own words, including the warning that transaction mode forbids session-based features, and the defaults (pool size 20, client cap 100) that everyone tunes upward.

Carry forwardRead the mode definitions before the benchmark posts; the constraints live here.
github.com
Vendor AWS Database Blog2020

Performance impact of idle PostgreSQL connections

Vendor-run but methodical: 1,000 idle connections on a db.m5.large cut mixed throughput 8.7% and select-only throughput 46%, by eating the OS page cache.

Carry forwardOn memory-constrained instances the idle tax lands on the page cache first.
aws.amazon.com
Vendor AWS RDS Proxy docscurrent

Avoiding pinning

The managed proxy's answer to session state: detect it and pin the client to a dedicated connection until the session ends, silently trading multiplexing for correctness.

Carry forwardMonitor pinning rate; a "pooled" fleet that is fully pinned is session mode at proxy prices.
docs.aws.amazon.com
07

Build a miniature, then productionise it

Six rungs from an evening experiment to an operable ration line. The crossing from toy to real happens at rung four.

Feel the ceiling

Local Postgres, default configuration. Write a script that opens connections until the server refuses, watching memory per backend as you go.

Done when: you have seen the refusal error text and can state your measured per-connection memory.  Teaches: the budget is real and smaller than folklore says.

Measure the idle tax

Run pgbench at fixed concurrency, then repeat with 1,000 idle connections held open by a second process. Try it on Postgres 13 and 16 if you can.

Done when: you have two throughput numbers and a delta.  Teaches: idle is not free, and the tax changed size in Postgres 14.

Race a big pool against a small one

One web app, one load generator, pool of 100 versus pool of 10 against the same 4-core database, measuring p99 at saturation.

Done when: the small pool wins at saturation and you can explain why in one sentence.  Teaches: the queue belongs in front of the database, not inside it.

Insert a transaction-mode pooler and break it

Put PgBouncer in the path. Reproduce a session-state leak: SET a timeout on one client and observe another inherit it; prepare a statement and watch it maroon on an old pooler version, then fix it with max_prepared_statements on 1.21+.

Done when: you have produced and then eliminated the marooned-statement error.  Teaches: pool mode is an application semantics change.

Run the storm drill

Under steady load, restart the pooler and the app fleet together. Watch the reconnection herd hit the database, then add jittered backoff and a connection-creation rate limit and run it again.

Done when: recovery no longer re-triggers the overload.  Teaches: the open-versus-closed distinction, physically.

Operate it

Export waiting clients, active server connections and checkout wait time. Set the saturation alert below the threshold you found in rung five's failures, and write the one-page runbook for the alert.

Done when: a synthetic overload pages you before the first user-visible error.  Teaches: the pooler is a capacity-planned service with its own SLO.

08

Keep hunting

The queries that actually found this material, grouped by what they surface. The vocabulary is the value: saturation, pinning, session state, SO_REUSEPORT.

Postmortems and incidents

  • pgbouncer postmortem OR "incident review" saturation site:gitlab.com
  • "connection pool" exhausted postmortem outage "we"
  • "active database connections" threshold "post-incident"
  • "database connection saturation" incident review

Source code and design arguments

  • repo:pgbouncer/pgbouncer is:pr is:closed is:unmerged prepared
  • pgbouncer "multi-threading" issue so_reuseport
  • "prepared statement" "S_1" "does not exist" pgbouncer
  • postgres commit "snapshot scalability" GetSnapshotData

Production experience

  • "we replaced pgbouncer" OR "outgrowing pgbouncer"
  • "single-threaded" pgbouncer "one core" scale "we"
  • "max_client_conn" latency degraded fleet
  • "connections per" instance capped re-shard pgbouncer

Mechanism and theory

  • "idle connections" postgres measured TPS memory impact
  • "open versus closed" workload connection pool collapse
  • RDS Proxy pinning "session state" multiplexing
  • connection pooler "prepared statements" comparison talk
09

References

Checked 2026-09-06. This guide was assembled in a sandbox that can fetch only github.com and gitlab.com directly; sources on other hosts were verified through live search retrieval of their exact wording, as recorded per-claim in the accompanying sources.md ledger.

  1. HikariCP wiki, About Pool SizingGitHub project wiki, undated. Checked 2026-09-06.
  2. Andres Freund, Analyzing the Limits of Connection Scalability in PostgresCitus Data / Microsoft, 2020-10-08. Checked 2026-09-06.
  3. PostgreSQL commit dc7420c2c92, snapshot scalability (Andres Freund)postgres/postgres, August 2020. Checked 2026-09-06.
  4. PgBouncer PR #845, Support of prepared statementsGitHub, opened 2023-05-14, merged 2023-10-05. Checked 2026-09-06.
  5. PgBouncer PR #757, server-side prepared statements cacheGitHub, opened 2022-08-18, closed unmerged 2023-08-29. Checked 2026-09-06.
  6. PgBouncer 1.21.0 release notes, "The one with prepared statements"GitHub, 2023-10-16. Checked 2026-09-06.
  7. PgBouncer issue #1021, Feature: multi-threadingGitHub, 2024-02-08, open. Checked 2026-09-06.
  8. PgBouncer configuration documentation (doc/config.md)GitHub, current master. Checked 2026-09-06.
  9. pgjdbc issue #869, PrepareThreshold=0 but prepared statement is storedGitHub, 2017-07-20. Checked 2026-09-06.
  10. GitLab runbooks, PgBouncer READMEGitLab, current master. Checked 2026-09-06.
  11. GitLab, More scalable database connection pooling (issue #6981)GitLab production-engineering tracker, undated. Checked 2026-09-06.
  12. GitLab production incident #7565, pgbouncer_client_conn_primary saturationGitLab, 2022-08-08. Checked 2026-09-06.
  13. Red Hat, About the Quay.io Outage: Post MortemRed Hat blog, 2020. Checked 2026-09-06 (search-verified).
  14. GitHub, February service disruptions post-incident analysisGitHub blog, 2020-03-26. Checked 2026-09-06 (search-verified).
  15. Honeycomb, Incident Review: What Comes Up Must First Go DownHoneycomb blog, 2023. Checked 2026-09-06 (search-verified).
  16. Honeycomb, Postmortem: RDS Clogs and Cache-Refresh Crash LoopsHoneycomb blog, 2019. Checked 2026-09-06 (search-verified).
  17. OpenAI, Scaling PostgreSQL to power 800 million ChatGPT usersOpenAI, 2026. Checked 2026-09-06 (search-verified).
  18. Figma, The growing pains of database architectureFigma blog, 2023, describing 2020. Checked 2026-09-06 (search-verified).
  19. Figma, PGKeeper: Building the Bouncer We Needed for PostgresFigma blog, 2026. Checked 2026-09-06 (search-verified).
  20. Notion, The Great Re-shardNotion blog, 2023-07-17. Checked 2026-09-06 (search-verified).
  21. Cloudflare, Pools across the seaCloudflare blog, 2025-04-08. Checked 2026-09-06 (search-verified).
  22. Supabase, Supavisor: Scaling Postgres to 1 Million ConnectionsSupabase blog, 2023. Checked 2026-09-06 (search-verified).
  23. Supabase, supavisor repository READMEGitHub, current master. Checked 2026-09-06.
  24. ClickHouse, How we scale PgBouncer in ClickHouse Managed PostgresClickHouse blog, undated (recent). Checked 2026-09-06 (search-verified).
  25. Crunchy Data, Postgres at Scale: Running Multiple PgBouncersCrunchy Data blog, 2022. Checked 2026-09-06 (search-verified).
  26. AWS, Performance impact of idle PostgreSQL connectionsAWS Database Blog, 2020. Checked 2026-09-06 (search-verified).
  27. AWS, Avoiding pinning an RDS ProxyAWS documentation, current. Checked 2026-09-06 (search-verified).
  28. Brandur Leach, How to Manage Connections Efficiently in Postgres, or Any Databasebrandur.org, 2018-10-15. Checked 2026-09-06 (search-verified).
  29. JP Camara, PgBouncer is useful, important, and fraught with periljpcamara.com, 2023-04-12. Checked 2026-09-06 (search-verified).
  30. Marc Brooker, Open and Closed, Omission and Collapsebrooker.co.za, 2023-05-10. Checked 2026-09-06 (search-verified).
  31. Schroeder, Wierman, Harchol-Balter, Open Versus Closed: A Cautionary TaleNSDI '06, USENIX, May 2006. Checked 2026-09-06 (search-verified).
  32. Barnhart et al., Resource Management in Aurora ServerlessPVLDB 17(12), 2024. Checked 2026-09-06 (search-verified).
  33. Jelte Fennema-Nio, Comparing Postgres connection pooler support for prepared statementsPOSETTE 2024, June 2024. Checked 2026-09-06 (search-verified).
  34. Bohan Zhang, Scaling Postgres to the next level at OpenAIPOSETTE 2025, June 2025. Checked 2026-09-06 (search-verified).