PostgreSQL Mastery
PostgreSQL Mastery lessons / Module 37: Connections and pooling

Connections and pooling

Level 7: Operations and HA20 min

Every PostgreSQL connection is a process with its own memory, so thousands of mostly idle connections waste resources and slow everything down. A pooler such as PgBouncer multiplexes many client connections over a small number of server connections.

This lesson is interactive. Run every example on a real PostgreSQL 18 server in your browser, read EXPLAIN ANALYZE plans, and take the quiz.
Open the interactive lesson
You will learn to
  • Explain why max_connections should stay modest
  • Choose between PgBouncer session, transaction and statement pooling
  • Know what breaks in transaction mode and how to size pools

conceptThe concept

Each backend uses a few megabytes plus whatever work_mem its queries take, and snapshot computation and lock management get slower as connections grow. Many servers do best with active connections around 2-4 times the CPU cores. Microservices with 50 pods times 20 connections each quickly exceed that.

PgBouncer pool modes: session (a server connection per client session; safe, little saving), transaction (a server connection only during a transaction; the usual choice), statement (per statement; no multi-statement transactions). In transaction mode, session state does not survive between transactions: session-level SET, advisory locks held across transactions, LISTEN, WITH HOLD cursors and temp tables need care. Protocol-level prepared statements are supported since PgBouncer 1.21 with max_prepared_statements.

Size pools from the database side: default_pool_size per database and user pair, plus a small reserve, well under max_connections. Application-side pools (HikariCP and others) should be small too.

whyWhy it matters

"Too many connections" outages and servers spending more time context-switching than working are among the most common PostgreSQL production issues, and pooling is the fix.

Use caseReal-world scenario

200 application pods each open up to 10 connections (2,000 clients). PgBouncer in transaction mode serves them with 40 server connections, and the database runs at max_connections = 200 with headroom for admin tools.

Try itExamples

Run and edit these in the interactive lesson.

Connections by state
SELECT state, count(*) AS connections,
       max(now() - state_change) AS longest_in_state
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state
ORDER BY connections DESC;
SHOW max_connections;
SELECT count(*) FILTER (WHERE backend_type = 'client backend') AS client_backends, count(*) AS all_processes FROM pg_stat_activity;
PgBouncer configuration
; pgbouncer.ini
[databases]
shop = host=10.0.1.1 port=5432 dbname=shop

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 40
reserve_pool_size = 5
max_prepared_statements = 200
server_reset_query =
server_check_delay = 10
Transaction-mode safe settings
BEGIN;
SET LOCAL statement_timeout = '5s';
SET LOCAL app.customer_id = '42';
SELECT current_setting('statement_timeout') AS timeout_in_tx, current_setting('app.customer_id') AS tenant;
COMMIT;
SELECT current_setting('statement_timeout') AS timeout_after, current_setting('app.customer_id', true) AS tenant_after;
SET LOCAL ends with the transaction, so the next client that gets this server connection from the pool does not inherit it.

Avoid / preferDo this, not that

Avoid
max_connections = 5000
Prefer
max_connections = 300
# PgBouncer in front: thousands of client connections, a few dozen server connections

Each backend is a process with its own memory, and contention grows with active connections. Throughput usually peaks at a few times the CPU core count.

Avoid
-- transaction pooling, and the app uses SET search_path and session advisory locks
Prefer
-- SET LOCAL inside transactions, transaction-level advisory locks,
-- or session pooling for the clients that need session state

In transaction mode, consecutive transactions can run on different server connections, so session state leaks between clients or disappears.

PitfallsCommon mistakes

  • Raising max_connections to 5,000 instead of pooling.
  • Using session-level SET, advisory locks or temp tables across transactions behind a transaction-mode pooler.
  • Giving every application instance a large pool, so the sum across instances exceeds what the database can handle.

LimitsWhen not to use it

Transaction-mode pooling does not suit clients that rely on LISTEN/NOTIFY, session temp tables, WITH HOLD cursors or session-level SET. Route those through session mode or directly.

SecuritySecurity notes

Encrypt both legs: client to PgBouncer and PgBouncer to PostgreSQL. With auth_query, use a SECURITY DEFINER lookup function owned by a restricted role instead of letting PgBouncer read pg_authid.

ChallengeTry it yourself

Show how many client connections are open, the maximum allowed and the percentage used.

Show one solution
One solution
SELECT count(*) AS connections,
       current_setting('max_connections')::int AS max_connections,
       round(100.0 * count(*) / current_setting('max_connections')::int, 1) AS pct_used
FROM pg_stat_activity
WHERE backend_type = 'client backend';

InterviewQuestions you should be able to answer

Why use PgBouncer, and which pool mode?
Because each PostgreSQL connection is a process with memory overhead and per-connection costs in snapshots and locks; thousands of mostly idle connections degrade performance. PgBouncer accepts many client connections and multiplexes them over a few server connections. Transaction mode is the usual choice: a server connection is assigned only for the duration of a transaction, giving the highest reuse. The cost is that session state does not persist across transactions (session SET, advisory locks, LISTEN, holdable cursors, temp tables), so applications must use SET LOCAL and avoid session features.
How would you size max_connections and pools?
Start from the database: aim for active connections a few times the number of CPU cores, keep max_connections modest (a few hundred at most) with headroom for admin and replication. Set PgBouncer default_pool_size per database/user so the total stays below that, and keep application pools small. Watch for clients waiting in PgBouncer (cl_waiting, avg_wait_time) and for idle-in-transaction sessions that pin server connections.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.

Go deeperOfficial documentation