Databases & reliability · 2026

Your Postgres will die at 50 concurrent users, not 50,000.
Here is the connection pooling guide nobody handed you.

Indie SaaS rarely runs out of CPU. It runs out of Postgres connections. Here is the math you should do tonight, the fix in the right order, and the PgBouncer config gotchas that quietly break Prisma, Django, and SQLAlchemy.

A pooler in front of Postgres by default.

Managed Postgres with built-in pooling, live pg_stat_activity in the dashboard, and Telegram alerts on too_many_connections — on every plan, including the always-on free tier.

No credit card required 1 service free forever Pooled Postgres included Telegram alerts on every plan
Frequently asked

Quick answers

Why does Postgres run out of connections before it runs out of CPU?

Every Postgres connection is a real OS process using 5 to 15 MB, so Postgres caps them with max_connections — often 20 on Heroku Basic, ~100 on a small VPS. Hit the cap and Postgres refuses with FATAL: too_many_connections rather than slowing down, so 50 concurrent users can exhaust the seats while CPU sits at 3%.

Why does this bite indie SaaS specifically?

Three things stack: serverless fan-out (a Next.js page load spins up 4 to 8 functions, each wanting a connection), generous ORM pool defaults (Prisma per-process, node-postgres 10, SQLAlchemy 5+10), and hot-reload leaks in dev. Big teams set up pooling years ago; indie founders hit the wall on launch day.

Do I always need PgBouncer to fix this?

No — pooling is the third fix. First lower your ORM's per-process pool size (connection_limit=2 for Prisma, max:3 for node-postgres, pool_size=2 max_overflow=0 for SQLAlchemy). Second, stop hot-reload leaks with a globalThis singleton. Third, hunt idle-in-transaction leaks in pg_stat_activity. Add a pooler only if you still need headroom.

Why do I get "prepared statement does not exist" after adding PgBouncer?

In pool_mode = transaction, a client can land on a different real connection per transaction, and prepared statements live on one connection. The fix for Prisma is ?pgbouncer=true (or statement_cache_size=0); Django with psycopg2 needs DISABLE_SERVER_SIDE_CURSORS = True. It is not a PgBouncer bug — your ORM assumed session mode.

How do I set default_pool_size safely?

Keep default_pool_size plus a buffer under max_connections. A safe rule is (max_connections - 5) divided by the number of user-database pairs. max_client_conn = 500 is the comfortable lie you tell the app; Postgres only ever sees default_pool_size real connections.