The error everyone eventually sees
FATAL: sorry, too many clients already (Postgres) or Too many connections (MySQL). It usually shows up under load, takes down half your endpoints, and is confusing the first time because the *database* is barely working — it's just refusing new connections. Understanding why is the key to never seeing it again.
Why databases cap connections
A database connection isn't free. In PostgreSQL especially, each connection is backed by a separate OS process with its own memory (work_mem allocations, caches, etc.). Even idle connections consume memory and add scheduling overhead. MySQL uses threads rather than processes, but the principle holds: each connection costs RAM and CPU.
So databases impose a hard cap (max_connections). It exists to protect the database from being overwhelmed — past a point, *more* connections make the database *slower* for everyone due to context switching and memory pressure, not faster. The cap is a guardrail, not an arbitrary limit.
The counterintuitive truth: a well-tuned Postgres often performs best with a relatively small number of active connections — frequently in the low dozens for many workloads, roughly scaling with available CPU cores, not hundreds. Throwing 500 connections at an 8-core box is a recipe for thrashing.
The math that bites you
Here's the trap. Say your database allows 100 connections. You run an app that opens a pool of 20 connections. Fine — until you scale out:
1 instance x 20 pool = 20 connections (ok)
5 instances x 20 pool = 100 connections (at the limit)
6 instances x 20 pool = 120 connections (FATAL: too many)Each app instance has its own pool, and the totals multiply. Autoscaling makes this worse — scale up under load and you can blow the connection cap *exactly when you need the database most*. This is the #1 way teams hit the wall: not one greedy app, but many modest pools summing past the limit.
Connection pooling: the core fix
A connection pool keeps a set of established connections open and reuses them across requests, instead of opening a new connection per request (which is slow — handshake, auth, process spawn). Every app should use a pool. But there are two layers of pooling:
Application-side pools
Your ORM/driver maintains a pool inside each app instance (pg.Pool, HikariCP, SQLAlchemy's pool, etc.). This solves per-request connection churn but doesn't solve the multiplication problem — N instances still equal N pools.
A dedicated pooler in front of the database
For real scale, put a pooler like PgBouncer (Postgres) between your apps and the database. All app instances connect to the pooler; the pooler maintains a much smaller pool of *actual* database connections and multiplexes thousands of client connections onto them.
App instances (many, big client pools)
|
v
PgBouncer (transaction pooling)
| small number of real connections
v
PostgreSQL (max_connections respected)In transaction pooling mode, a real DB connection is only held for the duration of a transaction, so a handful of backend connections can serve a huge number of clients. This is how you serve thousands of app-side connections against a database configured for, say, 100. The caveat: transaction pooling breaks session-level features (session variables, prepared statements in some setups, LISTEN/NOTIFY) — know your mode.
Sizing your pools
A practical sizing approach:
- 1Find the database's connection cap (and reserve headroom for admin/migration connections — never plan to use 100%).
- 2Decide max app instances (consider autoscaling ceiling).
- 3Per-instance pool size = (cap minus reserve) divided by max instances.
cap = 300, reserve = 30, max_instances = 10
per_instance_pool = (300 - 30) / 10 = 27With a pooler in front, you instead size the pooler's *backend* pool to the database and let clients connect generously. A common rule of thumb for the backend pool size is in the order of cores * 2 to cores * 4 for active connections, then validate under real load.
Tier-aware limits on managed databases
Managed platforms expose connection limits per plan, and you should plan against them. On PandaStack, managed databases (PostgreSQL, MySQL, MongoDB, Redis via KubeBlocks) come with tiered connection limits: 50 connections on Free, 300 on Pro, 1000 on Premium. That 50-connection free-tier number is exactly why pooling matters even for small projects — a couple of app instances with naive pools can exhaust it. Size your app pool small on Free (or front it with a pooler), and step up the tier as your concurrency grows.
| Plan | Connection limit | Practical posture |
|---|---|---|
| Free | 50 | Small pool per instance; pool aggressively |
| Pro | 300 | Room for several instances; still size deliberately |
| Premium | 1000 | High concurrency; pooler recommended at scale |
Operational checklist
- Always pool — never open a connection per request.
- Account for instance count — total connections = instances × pool size.
- Reserve headroom — leave room for migrations, admin, and monitoring connections.
- Use a dedicated pooler (PgBouncer) once you have multiple instances or autoscaling.
- Set sane idle timeouts so leaked/idle connections get reclaimed.
- Monitor active vs. max — alert before you hit the cap, not after.
- Cap your autoscaler so scaling out can't blow the connection limit.
Bottom line
Connection limits exist because connections are expensive, and you hit them not from one bad app but from many modest pools multiplying as you scale. The fix is layered pooling: small, well-sized application pools, and a dedicated pooler in front of the database once you have real concurrency. Size against your plan's limit with headroom to spare, monitor utilization, and cap your autoscaler. Do that and "too many connections" becomes a problem you read about instead of one you get paged for.
References
- [PostgreSQL — max_connections / Connection settings](https://www.postgresql.org/docs/current/runtime-config-connection.html)
- [PgBouncer — official documentation](https://www.pgbouncer.org/usage.html)
- [MySQL — max_connections](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_connections)
- [PostgreSQL Wiki — Number of database connections](https://wiki.postgresql.org/wiki/Number_Of_Database_Connections)
Want a managed database with sensible defaults and auto-wired DATABASE_URL? PandaStack includes one on the free tier — start here: [dashboard.pandastack.io](https://dashboard.pandastack.io)