awesome-everything RU
↑ Back to the climb

Databases

PgBouncer session, transaction, and statement modes

Crux Transaction mode gives 10–100× multiplexing but breaks SET, LISTEN, SQL PREPARE, and session-level advisory locks; each has a transaction-mode-safe equivalent.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 12 min

A team switches PgBouncer from session to transaction mode. Their deployed SET search_path = 'app, public' suddenly stops working. The app has been in production for six months. Nothing changed — except the pooler mode.

The three PgBouncer pooling modes

Session mode: a backend is leased to a client for the entire duration of the client’s connection. PgBouncer is essentially acting as a connection broker — 1:1 between client connections and backend connections. Everything works: SET, LISTEN, temp tables, session-level advisory locks, classic SQL PREPARE. Multiplexing is zero; you gain only auth offload and a connection queue.

Transaction mode: a backend is leased only for the duration of one transaction (BEGIN…COMMIT, or one autocommit statement). Between transactions the backend returns to the pool and can be handed to a different client. This is the mode that enables real multiplexing — a 10,000-client app can operate on 20–50 backends. The cost is that session state is not preserved across transaction boundaries.

Statement mode: backend returned after every single statement. Multi-statement transactions are forbidden. Used almost exclusively for PL/Proxy-style horizontal sharding; not a general-purpose mode.

The production default is transaction mode — session mode gives up the multiplexing win; statement mode is too restrictive for OLTP.

FeatureSession modeTransaction modeFix in transaction mode
Multiplexing ratioNone (1:1)10–100×
SET search_path at connectWorksLost after first transactionALTER ROLE app SET search_path = …
SET LOCAL inside a transactionWorksWorks (scoped to that transaction)
LISTEN / NOTIFYWorksBroken — NOTIFY lands on the wrong backendDedicated session-mode connection for LISTEN
SQL-level PREPARE name AS …WorksEXECUTE lands on different backendUse protocol-level prepared statements (driver-managed); PgBouncer 1.21+ handles these
pg_advisory_lock() session-levelWorksLock owned by whichever backend handled the callpg_advisory_xact_lock() (transaction-level)
Temp tablesWorksMay not survive to next transactionON COMMIT DROP or promote to real table

Why SET breaks in transaction mode

In session mode, the connection owned by a client is always the same backend. A SET search_path = 'app, public' issued at startup runs on backend B1 and stays on B1 forever.

In transaction mode, PgBouncer returns B1 to the pool after the first COMMIT. The next query from the same client may land on B2, which has never seen the SET. The setting is invisible — silently gone.

Fix: ALTER ROLE app SET search_path = 'app, public' — Postgres applies this to every new connection for that role, so every backend starts with the right setting regardless of which one is assigned.

Why LISTEN breaks, and the dedicated-connection fix

LISTEN channel registers the current backend to receive NOTIFY channel messages. In session mode the backend is permanent — events arrive reliably. In transaction mode the backend is returned to the pool between transactions. A NOTIFY lands on whatever backend Postgres picks, which may be serving a different client; the original listener never hears it.

Fix: open one dedicated connection that bypasses PgBouncer or uses session mode exclusively for LISTEN. Keep all OLTP queries on the transaction-mode pool. The two connection strings coexist.

Why this works

Why does PgBouncer even have session mode? For teams migrating gradually and for applications that genuinely require session semantics (legacy PREPARE, session advisory locks, long-lived temp tables). Session mode is the safe default; transaction mode is the performance upgrade that requires an application audit first.

PgBouncer mode comparison
Session mode: multiplexing
None (1:1)
Transaction mode: multiplexing
10–100×
Statement mode: use case
PL/Proxy sharding only
Default pool_size
20
Default max_client_conn
100
PgBouncer memory per pooled conn
~2 KB
Postgres backend memory
~5–10 MB
Prepared statements in tx mode
PgBouncer 1.21+
Quiz

Your team switches PgBouncer from session to transaction mode and `SET search_path = 'app, public'` issued at connect time stops working. Why?

Quiz

Best fix when an application on PgBouncer transaction mode needs LISTEN/NOTIFY for cache invalidation?

Order the steps

Order the things to verify when adopting PgBouncer transaction mode, from highest-leverage to lowest:

  1. 1 Audit the app for SET (not SET LOCAL) — migrate to ALTER ROLE or SET LOCAL
  2. 2 Audit for classic SQL PREPARE — switch to driver protocol-level prepared statements
  3. 3 Audit for LISTEN — isolate on a dedicated session-mode connection
  4. 4 Audit for pg_advisory_lock (session-level) — switch to pg_advisory_xact_lock
  5. 5 Enable max_prepared_statements 1000 (PgBouncer 1.21+) for protocol prepares
  6. 6 Set server_reset_query = DISCARD ALL if any state leakage is possible
  7. 7 Set idle_in_transaction_session_timeout in Postgres GUC as a safety net
Recall before you leave
  1. 01
    Explain what changes when an app moves from PgBouncer session mode to transaction mode — what works, what breaks, and what becomes possible.
  2. 02
    Why does LISTEN break in PgBouncer transaction mode and what is the correct architectural fix?
  3. 03
    What is server_reset_query = DISCARD ALL in PgBouncer and when must it be set?
Recap

PgBouncer offers three modes: session (1:1, zero multiplexing, everything works), transaction (10–100× multiplexing, requires app audit), and statement (only for sharding proxies). Transaction mode is the production standard. It breaks four patterns — SET outside a transaction (fix: ALTER ROLE), LISTEN (fix: dedicated session-mode connection), SQL-level PREPARE (fix: driver protocol-level prepared statements with PgBouncer 1.21+), and session-level advisory locks (fix: pg_advisory_xact_lock). Each break has an exact equivalent that is safe in transaction mode; none requires dropping the feature, only moving it.

Connected lessons
appears again in258
Continue the climb ↑Pool sizing: the (cores × 2) + spindles formula and the two-layer stack
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.