PostgreSQL Connection Pooling with PgBouncer — Transaction vs Session Pooling
Fast recall
Quick revision
Core answer
Whether you understand PostgreSQL's process-per-connection cost model, can size a pool correctly, know which features break in transaction pooling mode (prepared statements, LISTEN/NOTIFY, session GUCs), and can configure Spring Boot + HikariCP + PgBouncer end-to-end.
Turning up HikariCP `maximum-pool-size` to 200 'to handle load', exhausting PostgreSQL `max_connections`, and wondering why the database is stalling on semaphores — or enabling transaction pooling while leaving HikariCP prepared-statement caching on.
Explain the `(CPU cores × 2) + effective_spindle_count` sizing rule, the prepared-statement caveat with PgBouncer 1.21+ support, AWS RDS Proxy as a managed alternative, and `SHOW POOLS;` for live diagnostics.
Interview-ready guide
Interview answer
PostgreSQL uses one OS process per connection, not a thread. Each idle connection still costs five to ten megabytes of memory and takes up a slot against max_connections. Once you run a handful of pods each with a HikariCP pool, you are past the default limit.
PgBouncer is the external pooler
- It sits between the application and PostgreSQL, usually on port 6432
- It accepts many client connections and maintains a small pool of real PostgreSQL connections
- In transaction pooling mode, a server connection is held only while a transaction is running, so 30 real connections can serve around a thousand idle-between-transaction clients
Pooling modes
- Transaction pooling is best for stateless Spring Boot services
- Session pooling holds the connection for the whole client session, which gives full PostgreSQL compatibility but far less multiplexing
- Statement pooling is rarely useful because it breaks multi-statement transactions
What breaks under transaction pooling
- Server-side prepared statements — fixed in PgBouncer 1.21+ or by disabling client-side prepared statements in HikariCP
LISTEN/NOTIFY, session-levelSET, andpg_advisory_lock— use their transaction-scoped equivalents or switch to session pooling
Sizing
- Start at roughly two times the CPU cores plus effective spindle count
- Tune up only if PgBouncer's
SHOW POOLSshowscl_waitingis consistently non-zero
Two-tier pattern
- HikariCP manages the app-side pool, PgBouncer the server-side
- Monitor both: HikariCP pending connections means raise the app pool; PgBouncer
cl_waitingmeans raise the server pool - RDS Proxy is the managed alternative with IAM auth and transparent failover, at ~1–2 ms latency cost per query
PostgreSQL Connection Pooling with PgBouncer — Transaction vs Session Pooling
Direct Answer
PostgreSQL forks a backend process per connection (not a thread), consuming ~5–10 MB of RAM and a process slot each. Beyond ~500 connections a single instance wastes memory and schedules poorly. PgBouncer multiplexes many application connections onto a small pool of real PostgreSQL connections. Transaction pooling is optimal for stateless Spring Boot services — a server connection is held only for the duration of a transaction. The two-tier pattern — HikariCP → PgBouncer → PostgreSQL — is the production default.
Code Example
# pgbouncer.ini — production-oriented starting point
[databases]
; one entry per logical database; host/port target real PostgreSQL
app = host=postgres-primary.internal port=5432 dbname=app
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgbouncer_admin
stats_users = pgbouncer_stats
; ---- POOL MODE ----
; transaction: server connection held only during a transaction (best for stateless HTTP)
; session: held for entire client session (highest compatibility, less multiplexing)
; statement: released after every statement (breaks transactions, rarely used)
pool_mode = transaction
; ---- SIZING ----
; max client-side connections PgBouncer will accept (app side)
max_client_conn = 1000
; server-side pool per (database, user); start at (cores * 2) + spindles
default_pool_size = 30
; safety valve for bursty traffic above default_pool_size
reserve_pool_size = 10
reserve_pool_timeout = 3
; upper bound per database
max_db_connections = 100
max_user_connections = 100
; ---- TIMEOUTS ----
server_idle_timeout = 300
server_lifetime = 3600
query_wait_timeout = 30
client_idle_timeout = 0
idle_transaction_timeout = 60
; ---- PREPARED STATEMENTS (PgBouncer 1.21+) ----
; enable server-side prepared statement support in transaction mode
max_prepared_statements = 100
; ---- TLS ----
client_tls_sslmode = require
server_tls_sslmode = verify-full
server_tls_ca_file = /etc/ssl/certs/ca.pem
; ---- LOGGING / STATS ----
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60
Common Mistakes & Pitfalls
Turning up HikariCP maximum-pool-size to 200 'to handle load', exhausting PostgreSQL max_connections, and wondering why the database is stalling on semaphores — or enabling transaction pooling while leaving HikariCP prepared-statement caching on.
Check your understanding
Answer these without looking back at the article.
- What breaks under PgBouncer transaction pooling mode and how do you mitigate each issue?
- How do you size `default_pool_size` and how do you tell it is wrong?
- How does AWS RDS Proxy compare to self-managed PgBouncer?
- What HikariCP properties should you change when placing PgBouncer in front of PostgreSQL?
Related interview prep categories
1Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.