← Writing
Backend
July 10, 2026 · 4 min read48
PostgreSQL
Database
Scaling
NestJS
Prisma
PgBouncer

How Many Connections Can PostgreSQL Actually Handle?

Why raising max_connections isn't the fix, and how PgBouncer, read replicas, and smart pooling actually let a NestJS + Prisma + PostgreSQL stack scale past the default connection ceiling.

Abdulboriy Malikov

How Many Connections Can PostgreSQL Actually Handle?

The default limit

Out of the box, PostgreSQL ships with a hard ceiling: max_connections = 100. This isn't a soft warning — once every slot is taken, new connections fail immediately with a "too many clients already" error, regardless of how much CPU or memory the server has free. You can raise this value in postgresql.conf, but before doing that it helps to understand what each connection actually costs.

Why raising the number isn't free

Every PostgreSQL connection spawns its own OS process rather than a lightweight thread. Each one reserves several megabytes of RAM at minimum, adds CPU overhead from context switching once dozens are active, and competes for the shared buffer cache. Five hundred connections isn't five hundred cheap sockets — it can easily add up to multiple gigabytes of RAM just to keep idle connections alive, before a single query even runs. On a modest VPS this starves the database of the cache memory it needs to serve queries efficiently, and past a certain point throughput actually drops even though the server technically "supports" that many connections.

Connections versus throughput

A commonly cited rule of thumb for the ideal number of active connections is roughly twice the CPU core count, plus a small allowance for disk spindles. For a typical four-core server that lands somewhere around eight to twelve truly active connections for best throughput — far below the default of 100. The rest of an application's concurrency needs to be handled by pooling rather than by raw connection count.

The real fix: connection pooling

Instead of letting every request open its own connection to Postgres, a pooler sits in front of the database and multiplexes many client requests through a small set of real connections.

PgBouncer

PgBouncer is the standard choice for this job. It supports three pooling modes:

  • Session mode, where one client holds one server connection for its entire session

  • Transaction mode, where the connection returns to the pool as soon as a transaction completes

  • Statement mode, where it returns after every single statement

Transaction mode is the right choice for most REST or GraphQL APIs, since it lets a handful of real Postgres connections serve hundreds of concurrent requests — each request only holds a connection for the length of its transaction, not its entire lifecycle.

Wiring it into an ORM like Prisma

Most ORMs maintain their own connection pool on top of whatever they're pointed at. When PgBouncer runs in transaction mode, the ORM needs to be told to avoid session-level features like prepared statements, and its own pool size should stay modest rather than duplicating PgBouncer's pooling. A frequent mistake is running a large pool on both the ORM and PgBouncer, which just adds queueing delay rather than real capacity — the ORM's limit should be sized based on what Postgres itself can actually handle, not guessed independently.

Scaling beyond a single pooler

Once pooling is in place, the next steps roughly follow this order, from cheapest to most involved:

  1. Connection pooling in transaction mode, which resolves the large majority of "too many clients" errors on its own

  2. Read replicas, which offload read-heavy traffic like dashboards and reports while writes stay on the primary

  3. Vertical scaling of the database server itself, since more memory often improves caching more than raw connection headroom does

  4. Sharding or partitioning, which usually only becomes relevant at a scale far beyond what a single primary with replicas can handle

  5. Newer pooling tools built for multi-tenant or multi-database setups, worth evaluating once an application manages several separate databases

A practical checklist

  • Base max_connections on actual available memory, not on an arbitrary large number

  • Put a pooler like PgBouncer in transaction mode between the application and Postgres

  • Keep the ORM's own connection limit modest, remembering it multiplies across every running instance

  • Watch for connections left idle inside open transactions, which are usually an application bug rather than a scaling problem

  • Add a read replica before considering a second primary, since it's a much smaller architectural change

The takeaway

Raising max_connections treats the symptom, not the cause. Real scalability comes from pooling connections and shortening how long each one is held, which is the difference between a database that falls over under moderate load and one that stays healthy under far heavier traffic on the same hardware.