PgBouncer in front of Postgres
Connection pooling for Olympus
By default, Kratos and Hydra each open a pool of connections to Postgres. At low scale this is fine, at higher scale you'll hit Postgres's max_connections ceiling and pay heavy memory cost. PgBouncer is a connection pooler that fixes both.
When you need it
- More than ~100 concurrent backend connections.
- You see "remaining connection slots are reserved" errors.
- Memory usage on Postgres host is high (each backend connection = 10-20 MB).
Below 100 concurrent, native pooling in Kratos/Hydra is fine.
Architecture
Kratos (pool of 50) ──┐
Hydra (pool of 50) ──┼──► PgBouncer ──► Postgres (max 50 backend)
Athena (pool of 30) ──┘ (transaction pooling)PgBouncer multiplexes many client connections onto fewer Postgres backends. With transaction pooling: each Postgres backend serves a new transaction (often a new client) each time it's free.
Compose
Add to docker-compose.yml:
ciam-pgbouncer:
image: edoburu/pgbouncer:latest
environment:
DB_HOST: ciam-postgres
DB_PORT: 5432
DB_USER: olympus
DB_PASSWORD: ${POSTGRES_PASSWORD}
POOL_MODE: transaction
MAX_CLIENT_CONN: 1000
DEFAULT_POOL_SIZE: 25
SERVER_RESET_QUERY: DISCARD ALL
AUTH_TYPE: scram-sha-256
depends_on: [ciam-postgres]
ports:
- "6432:5432" # PgBouncer portPoint Kratos/Hydra/Athena at PgBouncer instead of Postgres:
# .env
KRATOS_DSN=postgres://olympus:${POSTGRES_PASSWORD}@ciam-pgbouncer:5432/olympus?sslmode=disable
HYDRA_DSN=postgres://olympus:${POSTGRES_PASSWORD}@ciam-pgbouncer:5432/olympus?sslmode=disable
ATHENA_DB_URL=postgres://olympus:${POSTGRES_PASSWORD}@ciam-pgbouncer:5432/olympus?sslmode=disableNote sslmode=disable between app and pgbouncer (they're on the same host, same Docker network). Postgres-side SSL is between PgBouncer and Postgres, configured separately.
Pool modes
- Session pooling: client owns a backend until disconnect. Like no pooling. Best for clients that use SET commands, listen/notify.
- Transaction pooling: backend assigned per transaction. Best multiplexing. Use this for Olympus.
- Statement pooling: backend per statement. Forbids multi-statement transactions. Don't use.
Olympus's Go services (Kratos, Hydra) use simple transaction patterns, transaction pooling works.
Pool sizing
MAX_CLIENT_CONN: 1000 # how many client connections we accept
DEFAULT_POOL_SIZE: 25 # backend connections per database+user
RESERVE_POOL_SIZE: 5 # emergency overflowSo at most ~30 backends to Postgres for olympus user. Set Postgres max_connections = 100 to leave room for admin sessions.
Caveats
Transaction-scope features
Anything that requires session affinity breaks in transaction pooling:
LISTEN/NOTIFY(rare in Olympus).- Prepared statements named at session level (modern drivers handle this).
- Temporary tables.
Olympus's services don't use these. If you write custom hooks/webhooks that use them, use a separate connection pool that bypasses PgBouncer.
Reset query
SERVER_RESET_QUERY: DISCARD ALL runs after each transaction to clean up state (session variables, prepared statements). Required for transaction pooling. Cheap.
Auth
PgBouncer needs to authenticate clients. Easiest: same password as Postgres, configured via env var.
Alternative: auth_file with hashed passwords. More secure but more setup.
Monitoring
-- From PgBouncer admin port (default disabled; enable in config)
\c pgbouncer
SHOW POOLS;
SHOW STATS;Watch:
cl_active: active client connections.sv_active: active server (backend) connections.maxwait: longest a client waited for a backend (should be < 100ms).
Prometheus exporter: prometheuscommunity/postgres_exporter includes PgBouncer.
Tuning
If you see maxwait > 1s regularly:
- Increase
DEFAULT_POOL_SIZE. - Profile slow queries, they hog backends.
If you see cl_waiting > 0:
- Pool is saturated. Same fix as above.
If Postgres complains "too many connections":
- Lower
DEFAULT_POOL_SIZE× number of dbs/users to fit withinmax_connections.
Read replicas
PgBouncer can route read queries to a replica. Configure separate pools:
# pgbouncer.ini
[databases]
olympus = host=ciam-postgres dbname=olympus
olympus_ro = host=ciam-postgres-replica dbname=olympusThen in your service config, point read-heavy paths at olympus_ro. Olympus doesn't separate read/write by default, manual.