PostgreSQL Connection Pooling Transaction Mode vs Session Mode
Quick answer
PostgreSQL handles each client connection with a dedicated server process, and every one of those processes consumes memory and adds scheduling and locking...
PostgreSQL handles each client connection with a dedicated server process, and every one of those processes consumes memory and adds scheduling and locking overhead. An application with hundreds of instances, each holding a pool of connections, can exhaust the server long before the CPU is busy. External connection poolers solve this by multiplexing many client connections onto a small number of real database connections, and their most important setting is the pooling mode. Pick the wrong one and you get subtle bugs instead of performance.
Why Connection Counts Matter
It is tempting to raise max_connections whenever the server runs out of slots, but more connections rarely means more throughput. Beyond a point, concurrent active queries compete for CPU, memory, and disk, and context switching erodes performance. Many guidelines suggest keeping the number of actively working connections close to a small multiple of the available CPU cores, though the right value depends on your workload and storage, so measure it.
The result is a mismatch: applications want lots of connections for convenience, while the database performs best with few. A pooler sits in between and lends out server connections only when a client actually needs them.
Session Pooling
In session mode, a client is assigned a server connection when it connects and keeps it until it disconnects. Everything that works on a direct connection works here: session-level settings, advisory locks held across statements, temporary tables, LISTEN and NOTIFY, and cursors that outlive a transaction.
The cost is that it multiplexes poorly for long-lived clients. If your application holds a connection open for its whole lifetime, as most application-side pools do, each client occupies a server connection permanently, and the pooler gives little benefit beyond absorbing connection storms and reusing connections between short-lived clients. It is the safest mode and the least efficient.
Transaction Pooling
In transaction mode, a server connection is assigned to a client only for the duration of a transaction. When the transaction commits or rolls back, the connection returns to the pool and may serve a different client on the next transaction. This is where the large efficiency gains come from, because idle clients hold no server connection. Thousands of mostly idle application connections can share a few dozen real ones.
The price is that anything depending on session state across transactions can break, because the next transaction may run on a different server connection. Features to review before enabling it:
- Session settings: a plain SET command applies to the session and may leak to another client or disappear. Use SET LOCAL inside a transaction, or configure parameters per role or database.
- Session-level advisory locks: locks acquired outside a transaction can be held by a connection that is then lent to someone else. Use transaction-level advisory locks instead.
- LISTEN and NOTIFY: listeners need a persistent session, so they should connect directly or through a session-mode pool.
- Temporary tables and cursors held across transactions: these depend on staying on one connection and generally do not work reliably.
- Prepared statements: server-side prepared statements are tied to a connection. Support in transaction mode depends on the pooler and its version, and many drivers need a setting to avoid named prepared statements. Check the pooler documentation for your version.
BEGIN;
SET LOCAL statement_timeout = '5s';
SET LOCAL app.tenant_id = '42';
SELECT * FROM invoices WHERE tenant_id = current_setting('app.tenant_id')::bigint;
COMMIT;
Statement Pooling and Other Variants
Some poolers also offer statement mode, where a connection is returned after every statement. It is the most aggressive, and it disallows multi-statement transactions, so it applies only to narrow cases with autocommit workloads. For most applications the real decision is between session and transaction pooling, and a mix is common: transaction mode for the main application traffic, and a separate direct or session-mode path for migrations, administrative tasks, and anything that listens for notifications.
Sizing the Pools
Two sets of numbers matter: how many client connections the pooler accepts, and how many server connections it opens per database and user. The first can be large, since idle clients are cheap. The second should be small and chosen with the database's capacity in mind. Remember to account for every pooler instance, every database and user pair, and reserve connections for administration and monitoring, so the sum stays below max_connections.
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 20
reserve_pool_size = 5
Treat this configuration as an illustration, not a recommendation, and tune the values against measured load. Also review timeouts, such as how long a client waits for a server connection, because a small pool under heavy load makes clients queue, and the queueing behavior is part of your latency profile.
Application Pools Versus External Poolers
Application-side pools, built into most drivers and frameworks, are still useful: they avoid connection setup cost and bound each process. They do not solve the problem of many processes multiplying the total. Use both together, keeping each application's pool modest and letting the external pooler cap the total reaching the database.
Frequently Asked Questions
Which mode should I start with?
Start with session mode if you are unsure, since it behaves like a direct connection. Move to transaction mode after verifying that your application does not rely on session state between transactions.
Why do my queries sometimes use the wrong settings in transaction mode?
A session-level SET stayed on a server connection that was reused by another client. Use SET LOCAL inside transactions or role-level defaults.
Can I use prepared statements with transaction pooling?
It depends on the pooler version and driver. Some combinations support protocol-level prepared statements, while others require disabling them. Check both the pooler's and the driver's documentation.
Does a pooler replace tuning max_connections?
No. You still need a sensible server limit. The pooler lets many clients share it more efficiently.
Summary
Session pooling is safe and compatible but saves little for long-lived clients, while transaction pooling provides large multiplexing gains at the cost of breaking session-dependent features. Audit your use of session settings, advisory locks, notifications, and prepared statements before switching, keep server-side pools small, and use direct connections for administrative work. Verify mode semantics in the documentation of the pooler and PostgreSQL version you run.