PostgreSQL Connection Pooling: PgBouncer Guide

Spike to 500 Postgres connections? You're torching 5GB RAM before queries even fire. PgBouncer reclaims it all—here's the proof.

Postgres 'Too Many Connections' Crisis: PgBouncer Crushes It, Supavisor Tries Harder — theAIcatchup

Key Takeaways

  • Postgres connections eat 5-10MB RAM each—pool to reclaim 90% overhead.
  • PgBouncer dominates with battle-tested transaction pooling; cap at cores*2.
  • Supavisor fits multi-tenant; built-in Postgres lags—poolers essential now.

FZZT. ‘Too many connections.’ Your app crashes mid-traffic surge.

And just like that, hours debugging what PostgreSQL connection pooling could’ve prevented in seconds. Every single Postgres link spawns an OS process—5-10MB RAM apiece. Crank max_connections to 200? Kiss 2GB goodbye before a query touches data. At 500, it’s 5GB flushed on infrastructure alone. Brutal math, right?

Servers don’t scale linearly. Throw in 20 container replicas, each nursing 10 persistent links—bam, 200 baseline. Rolling deploys double it temporarily. Serverless? Hundreds of fleeting invocations per second, each forging its own bond. Each handshake? 50-100ms of forking, auth, catalog loads, TLS chit-chat. Not cheap.

Two cliffs await: connection caps slamming shut, or RAM evaporating—starving shared_buffers, work_mem, page cache. I’ve seen prod clusters OOM because devs juiced max_connections like it was free candy. Symptom-chasing at its dumbest.

The Real Enemy: Idle Connections Hoarding Slots

Run this query—ruthless truth serum:

SELECT state, usename AS username, application_name, count() AS connection_count, count() FILTER (WHERE state = ‘idle’) AS idle_connections, count(*) FILTER (WHERE state = ‘idle in transaction’) AS idle_in_transaction FROM pg_stat_activity WHERE backend_type = ‘client backend’ GROUP BY state, usename, application_name ORDER BY connection_count DESC;

High idle counts scream for pooling. Those ‘idle in transaction’ zombies? Worst. They grip locks, snapshots, pool spots—for minutes. Hunt ‘em:

SELECT pid, usename, application_name, state, now() - state_change AS idle_duration, left(query, 100) AS last_query FROM pg_stat_activity WHERE state = ‘idle in transaction’ AND now() - state_change > interval ‘5 minutes’ ORDER BY state_change;

Above 70% max_connections utilization? Spikes will kill you. Check it:

SELECT count() AS active_connections, current_setting(‘max_connections’)::int AS max_connections, round(100.0 * count() / current_setting(‘max_connections’)::int, 1) AS utilization_percentage FROM pg_stat_activity WHERE backend_type = ‘client backend’;

Pooling flips the script: fixed DB pool (say, 20) juggles hundreds of app links. Most sit idle anyway—perfect reuse.

Truly active rarely tops CPU cores. On 8-core rig:

SELECT count() FILTER (WHERE state = ‘active’) AS truly_active, count() FILTER (WHERE state = ‘idle’) AS idle, count() FILTER (WHERE state = ‘idle in transaction’) AS idle_in_txn, count() AS total FROM pg_stat_activity WHERE backend_type = ‘client backend’;

If truly_active hovers under 20, cap your pool there. Formula? (Cores * 2) + disks. SSD cluster: ~17. Postgres parallelizes to cores—extra links just thrash context switches.

Why Does PostgreSQL Connection Pooling Matter for Scale-Out Apps?

Modern stacks explode links. Kubernetes? Replicas hoard ‘em. Serverless? Ephemeral frenzy. Without pooling, you’re pre-paying RAM for ghosts.

Enter PgBouncer—lightweight champ, deployed everywhere. Battle-tested since 2007 (yeah, older than Docker). Config’s a breeze:

[databases] myapp = host=127.0.0.1 port=5432 dbname=myapp

[pgbouncer] listen_addr = 0.0.0.0 listen_port = 6432 auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt pool_mode = transaction default_pool_size = 20 min_pool_size = 5 reserve_pool_size = 5 reserve_pool_timeout = 3 max_client_conn = 1000 max_db_connections = 50 server_idle_timeout = 600 query_timeout = 300

Transaction mode maximizes churn—returns links post-tx. Downside? No session stickiness. Forget prepared statements spanning txns, SETs, LISTEN/NOTIFY, temp tables. Statement mode looser, session strictest—but hungriest.

Peek inside: psql -p 6432 -U pgbouncer pgbouncer, then SHOW POOLS;. Stats reveal hits, waits, evictions. Tune ruthlessly.

Here’s my take: PgBouncer’s simplicity crushes. No Elixir runtime bloat—just C, lean as hell. On-prem, monolith, or modest K8s? It’s king. (Supabase folks hype theirs, but let’s check data.)

Supavisor.

Supabase’s Elixir-built contender. Multi-tenant focus: per-tenant pools, isolation. Threads handle client floods better than PgBouncer’s single-threaded core.

tenants: - id: “my-tenant” db_host: “localhost” db_port: 5432 db_database: “myapp” pool_size: 20 mode: “transaction”

Wins: named preps in tx mode (PgBouncer balks), tenant silos for noisy neighbors. But—it’s Elixir. Heavier footprint. Optimized for Supabase cloud, not your homelab.

Use alongside app-level pools, not solo. Python example cuts off in docs, but point stands: layer it.

PgBouncer vs Supavisor: Pick Wrong and Pay in Latency

PgBouncer: 90% market share, zero-downtime swaps. Supavisor: fresh (2023), multi-tenant magic—but benchmarks? PgBouncer edges throughput on single DBs. Supavisor shines at 1000+ tenants.

Unique angle: This echoes MySQL’s ProxySQL rise in 2010s—specialized poolers beat built-ins till DB cores caught up. Postgres 17 whispers built-in pooling upgrades, but they’re embryonic. Prediction: By 2026, serverless Postgres (Neon, Supabase) mandates multi-tenant like Supavisor—PgBouncer adapts or fades for cloud natives. On-prem? PgBouncer reigns another decade.

Don’t sleep on built-in. Postgres idles connections smarter now, but no true proxy multiplexing. Poolers remain essential.

Short version: Start PgBouncer. Scale to Supavisor only if tenants fight.


🧬 Related Insights

Frequently Asked Questions

What is PostgreSQL connection pooling and do I need it?

It shares a fixed set of DB connections across many app requests, slashing RAM and ‘too many connections’ errors. Need it? If utilization >50% or idle counts spike—yes.

PgBouncer vs Supavisor: Which is better?

PgBouncer for simplicity, speed, legacy stacks. Supavisor for multi-tenant clouds with prepared stmt needs. Benchmark your load.

How to size my Postgres pool?

(CPU cores * 2) + disks. Monitor truly_active queries—don’t exceed that by much.

Sarah Chen
Written by

AI research editor covering LLMs, benchmarks, and the race between frontier labs. Previously at MIT CSAIL.

Frequently asked questions

What is PostgreSQL connection pooling and do I need it?
It shares a fixed set of DB connections across many app requests, slashing RAM and 'too many connections' errors. Need it? If utilization >50% or idle counts spike—yes.
PgBouncer vs Supavisor: Which is better?
PgBouncer for simplicity, speed, legacy stacks. Supavisor for multi-tenant clouds with prepared stmt needs. Benchmark your load.
How to size my Postgres pool?
(CPU cores * 2) + disks. Monitor truly_active queries—don't exceed that by much.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by dev.to

Stay in the loop

The week's most important stories from theAIcatchup, delivered once a week.