Skip to the content
Platforms

Connection Pooler vs Read Replica: First, Check What the Server Is Actually Waiting On

The "pooler or replica" debate usually starts one step too late. Before you choose between PgBouncer and a standby instance, you need to know whether your database is drowning in connections or suffocating under workload. The server itself tells you which: check pg_stat_activity for the wait_event field.

6 min read

A manifold splitting one supply line into several outlets
A manifold splitting one supply line into several outlets. Photo: Bill Abbott · Wikimedia Commons · CC BY-SA 2.0

If connections pile up with ClientRead or idle states, you have a connection pressure problem. If they show IO::FileRead, Lock waits, or active query execution, you have a query pressure problem. The fix for the first looks nothing like the fix for the second. Buying a replica to solve connection saturation wastes money. Installing a pooler for a missing index wastes engineering hours.

Connection Pressure Is About Contention, Not Speed

Connection pooling exists because PostgreSQL spawns one process per client connection, and each process consumes memory. At several hundred concurrent connections, the overhead of process management—context switches, memory allocation, the spinlock storm on shared buffers—overwhelms whatever work the queries actually do. A connection pooler like PgBouncer sits between applications and the database, holding a smaller number of actual server connections and multiplexing many client connections across them.

PgBouncer offers three pooling modes, and the choice determines what your application can still do. In session pooling, a server connection stays bound to one client for the entire duration of that client's connection. When the client disconnects, the server connection returns to the pool. This preserves session state—temporary tables, advisory locks, SET variables, prepared statements—but largely defeats the purpose of pooling, since you still need roughly as many server connections as concurrent clients. Google Cloud's managed connection pooling documentation notes that session pooling "maintains session state but reduces pooling efficiency."

Transaction pooling, the default recommended by Google Cloud for short-lived connections, assigns a server connection only for the duration of a single transaction. When COMMIT or ROLLBACK executes, the connection goes back to the pool. This dramatically shrinks the server connection count but carries a cost: nothing persists across transaction boundaries. IBM Cloud's documentation warns that transaction pooling does not carry over session state such as SET variables, temporary tables, advisory locks, or LISTEN channels. Neon's documentation adds that transaction mode does not support SQL-level PREPARE/DEALLOCATE on pooled connections. If your application prepares a statement in one transaction and executes it in another, the prepared statement may not exist on the connection that serves the second transaction.

Statement pooling, the most aggressive mode, returns the server connection to the pool immediately after each query completes. Multi-statement transactions are explicitly disallowed because they would break: the second statement of a BEGIN... COMMIT block would likely run on a different server connection than the first. Statement pooling maximizes connection reuse but restricts you to autocommit-style, single-statement operations.

The Features You Lose Depend on the Mode You Choose

The practical impact of pooling mode selection manifests in specific application breakages. Session pooling breaks almost nothing but saves almost no resources. Transaction pooling, the common default, breaks prepared statements that span transactions, temporary tables that should persist across multiple transactions in a session, and any logic relying on advisory locks for application-level coordination. Snowflake's Postgres connection pooling documentation notes specifically that SQL-level prepared statements created with PREPARE and run with EXECUTE in different transactions will not work in transaction pooling because they may execute on different server connections.

Neon's documentation confirms that SET commands, temporary tables, and prepared statements all fail under transaction pooling. IBM Cloud's documentation lists the same session-state features as non-persistent. The pattern is consistent across vendors: transaction pooling gives you connection efficiency by stripping away session abstraction. If your ORM or framework assumes it can prepare a statement once and reuse it, or if your application uses temporary tables for intermediate results, transaction pooling requires code changes. Statement pooling requires more drastic constraints: no multi-statement transactions at all.

The diagnostic question—connection pressure versus query pressure—matters here because pooling solves only connection pressure. A slow query running through a pooler is still a slow query. The pooler may prevent the database from crashing under connection count, but it will not reduce the CPU, I/O, or lock contention that query generates.

Replicas Address Read Load, Not Query Performance

A read replica accepts the replication stream from the primary and serves SELECT queries. The replica's hardware runs those queries, offloading CPU and I/O from the primary. What a replica does not do is make a slow query fast. If the query lacks an index and performs a sequential scan, it performs that sequential scan on the replica instead. The replica may prevent the primary from saturating, but the user still waits.

Replicas introduce their own correctness constraints. Appwrite's PostgreSQL connection pooling documentation, which covers read/write splitting, notes that replicas are asynchronous by default. A write to the primary does not instantaneously appear on the replica. The documentation warns explicitly that "a read that immediately follows a write can be stale." The lag varies—milliseconds to seconds depending on write volume, network latency, and replication configuration—but it is never zero.

Application architectures handle this through two main patterns. One routes a user's own reads to the primary for some window after that user performs a write, accepting that cross-user reads may see older data. The other accepts staleness explicitly and surfaces it in the interface—"last updated 30 seconds ago" or similar. Neither pattern is automatic. Both require application-layer routing logic that knows which queries need which consistency level.

The replica-or-pooler choice, then, maps to the pressure type. Connection pressure: pool. Read-heavy workload that can tolerate stale data: replica. Write-heavy workload with strict read-after-write requirements: neither helps much; you need query optimization or architectural change.

The Risk of Solving the Wrong Problem

Teams frequently sequence these decisions incorrectly. A slow query prompts a replica purchase; the query still runs slowly, now on more expensive infrastructure. Connection spikes prompt query optimization; the connection count stays high, the database still falls over. The server wait state provides the corrective: ClientRead and idle connection accumulation mean you are not getting to the work. Active waits on locks, buffers, or I/O mean the work itself is the problem.

Neither tool addresses data model limitations. A query that requires multiple round-trips, that builds large intermediate result sets, that cannot use available indexes—these remain slow regardless of where they run. Connection pooling and read replicas are operational interventions. Query structure and schema design are developmental interventions. The operational spend buys headroom. The developmental work buys efficiency.

What the Choice Actually Costs

Between pooling modes, the cost is feature compatibility. Session pooling preserves your application logic but demands server connection counts that may exhaust memory. Transaction pooling demands code that tolerates stateless connections. Statement pooling demands single-statement autonomy. Between pooling and replication, the cost is consistency. Replicas trade currency for capacity; the asynchronous default means accepting that your read of your own write may not reflect your write.

The decision framework runs: diagnose the wait state, match the tool to the pressure type, accept the constraints the tool imposes. Connection pooling does not accelerate queries. Read replicas do not synchronize instantly. Both extend runway. Neither substitutes for fixing what the server actually waits on.

Sources

  1. Postgres Pro Standard Documentation — postgrespro.com, 2026-10-05
  2. Google Cloud SQL Documentation — docs.cloud.google.com, 2026-06-21
  3. Neon Docs — neon.com, 2026-09-30
  4. Snowflake Docs — docs.snowflake.com, 2026-10-07
  5. Appwrite Docs — appwrite.io, 2026-10-08
  6. IBM Cloud Docs — cloud.ibm.com, 2026-10-07

More from Platforms & Engineering

Section index

Independent trade desk. We take no commission on anything we describe and run no affiliate programme of our own. Every figure on this page names the standard, filing or organisation it comes from; where a number could not be verified the page says so. How we work and how we correct. Reviewed:

Cookies, and what this site stores. The Dispatch sets no advertising or analytics cookies and loads no third-party tracker. Closing this notice writes one key — icd-notice — into your browser’s local storage, so that the notice does not return. Nothing else is kept. The one thing a page here sends onward is what a reader types into the form on the contact page, and that is described before the form is used. What the policy says.