Skip to main content
RunBook Academy

PostgreSQLIV · Connections, Sessions and PoolingConnections

PgBouncer in production

Advanced⏱ ~25 min🧪 Lab requiredPgBouncerpsql

What you'll learn

  • Place a pooler in an architecture without creating a single point of failure
  • Reason about authentication when the pooler sits between client and server
  • Monitor the pooler as a distinct component from the database
  • Diagnose whether latency originates at the pooler or beyond it

Prerequisites

Practice

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

Adding a pooler solves the connection problem and introduces a component. That component sits in the request path of every query, has its own failure modes, and is frequently deployed once and never thought about again.

The version this course targets is PgBouncer 1.25.2, released 2026-05-08. That release is itself a security release addressing several CVEs, which is the first point worth making: a pooler is software in the data path and needs patching like anything else.

Where to put it

flowchart TD
    subgraph A["Sidecar: one pooler per application instance"]
      A1["app replica + pooler"] --> DB1["PostgreSQL"]
      A2["app replica + pooler"] --> DB1
    end
    subgraph B["Shared tier: a pooler service"]
      B1["app replicas"] --> BP["pooler (HA pair)"] --> DB2["PostgreSQL"]
    end
    subgraph C["On the database host"]
      C1["app replicas"] --> CP["pooler on DB host"] --> DB3["PostgreSQL"]
    end

Sidecar, one per application instance. No single point of failure and no extra network hop. The drawback is arithmetic: each pooler has its own pool, so the database sees replicas multiplied by pool size, and the multiplication the pooler was meant to provide is diluted. It works when replica count is small and stable.

A shared pooler tier. Achieves the full multiplication, because one pool serves everything. It is a component that must itself be highly available, and it adds a network hop to every query. This is the common production choice.

On the database host. Removes the network hop between pooler and database and ties the pooler’s availability to the database’s, which is reasonable since one is useless without the other. It does mean the pooler competes for the database host’s CPU, and PgBouncer is single-threaded per process, so a busy pooler can saturate one core while the host looks idle.

Authentication with something in the middle

The pooler terminates the client connection and opens its own to PostgreSQL, so there are two authentication events and they can use different credentials.

The consequences worth planning for:

PostgreSQL sees the pooler’s address, not the client’s. Every client_addr in pg_stat_activity becomes the pooler’s, which is why the earlier lesson insisted on application_name. Your pg_hba.conf rules now describe the pooler rather than the applications, so address-based restrictions move to the pooler’s own configuration.

The pooler needs credentials for every role it proxies. PgBouncer supports looking these up from PostgreSQL through auth_query, which avoids maintaining a parallel credential file, and that lookup itself needs a role with permission to read the authentication catalogue. Granting that carefully matters, because it is a role that can read password hashes.

TLS terminates at the pooler unless deliberately configured otherwise, so the client-to-pooler and pooler-to-database legs are separate TLS decisions. A deployment that encrypts the first and leaves the second in clear has encrypted the leg that was probably already inside the trust boundary and left the one crossing it.

Monitoring the pooler as its own component

PgBouncer’s admin console is a pseudo-database reached with an ordinary PostgreSQL client, and it answers the question no PostgreSQL metric can: is the wait happening before the database or inside it?

-- Connect to the pgbouncer pseudo-database, then:
SHOW POOLS;      -- per database and user: waiting clients, active servers, maxwait
SHOW SERVERS;    -- the connections to PostgreSQL and their state
SHOW CLIENTS;    -- the connections from applications
SHOW STATS;      -- request rates and average query and wait times

SHOW POOLS carries the two columns that matter most:

  • cl_waiting — clients that have work and no server connection. Any sustained non-zero value means the pool is the constraint.
  • maxwait — how long the longest-waiting client has been waiting. This is the pooler-side latency contribution, in seconds, and it is invisible from PostgreSQL entirely.

A maxwait of eight seconds with a healthy database means the application’s latency is being created by the pool size, not by query performance. Nobody looking only at PostgreSQL would ever see it, and the natural conclusion from database metrics alone — everything looks fine — is actively misleading.

Availability

A shared pooler tier is in the path of every query, so its availability becomes the database’s availability from the application’s point of view.

The patterns that work are the ordinary ones: two or more pooler instances behind a virtual IP or a load balancer, with health checks that verify the pooler can actually reach PostgreSQL rather than merely that its port is open. A pooler accepting connections while unable to reach the database is worse than one that is down, because it accepts work it cannot serve.

The failover interaction matters too, and Part XV returns to it: when the database fails over, the pooler’s existing server connections point at the old primary. Whether the pooler notices, how quickly, and whether it drops client connections in the process are properties to establish by testing rather than by reading, because they determine what the application experiences during a promotion.

Production discipline

  1. Patch the pooler. It is software in the data path; 1.25.2 is itself a security release.
  2. Monitor cl_waiting and maxwait from SHOW POOLS. They are the only evidence distinguishing pool-side latency from database latency.
  3. Watch per-process CPU. PgBouncer is single-threaded, so one saturated core on a many-core host is invisible in host averages.
  4. Configure both TLS legs deliberately and verify the pooler-to-database leg from the database with pg_stat_ssl.
  5. Health-check the pooler’s ability to reach PostgreSQL, not just its listening port. Accepting work it cannot serve is worse than being down.
  6. Test what a database failover does to the pooler before you need to know.

Cross-course references

  • Secrets, PKI & Certificate Management — Part VII (TLS for operators) covers the two-leg TLS question, and Part XIII (Dynamic credentials) covers issuing the credentials the pooler presents.
  • Kubernetes for Production Sysadmins — Part XXXVIII (Services) and Part XXXIV (PDB) cover running a pooler tier with availability guarantees.
  • Observability for Production Sysadmins — Part IX (Exporters) covers scraping the pooler’s admin console into metrics so the diagnostic split above is available on a dashboard.

Quiz

Knowledge check · 6 questions

  1. Q1. Application latency is high. PostgreSQL shows few active sessions and no waits of interest. SHOW POOLS reports cl_waiting of 40 and maxwait of 8. What is the constraint?

  2. Q2. A deployment configures TLS between the application and PgBouncer, and reports that database traffic is now encrypted. What has been missed?

  3. Q3. Which are genuine consequences of placing a pooler between applications and PostgreSQL? Select all that apply.

  4. Q4. A health check that confirms PgBouncer is accepting connections on its port is sufficient to consider it healthy.

  5. Q5. Explain why PgBouncer being single-threaded is a capacity property that host-level monitoring will miss.

  6. Q6. Diagnose the layer responsible and give the order of investigation.

    An application reports p99 latency of 9 seconds. The database dashboard shows CPU at 30%, no long-running queries, 40 active sessions against a max_connections of 400, and no notable wait events. The team has spent two hours tuning queries with no effect. The architecture places a single PgBouncer instance on a dedicated 16-core host between 60 application replicas and the database. Nobody has looked at the pooler because it has not been changed in a year.

Passing score: 75%. Answers are checked in this browser.