Journal / Performance Optimization

Performance Optimization

Connection Pooling: Why Defaults Are Wrong for Most Stacks

Framework default connection pool sizes are designed for a generic workload. Yours is not generic. The right pool size depends on your concurrency, your database capacity, your query latency, and your worker count. The defaults are usually too small for production loads and sometimes too large for the database to support. Picking the right numbers requires a small amount of measurement and a small amount of math.

What you actually need to know

  • Framework defaults are conservative and almost never right.
  • Size the pool for your concurrency at the 99th percentile.
  • Verify total connections fit under the database limit.
  • Long queries need a larger pool relative to worker count.
  • Each service has its own pool. Total budget across services matters.

Stack

Default

Typical correct value

Node.js pg

10

20 to 50

Rails ActiveRecord

5

15 to 40

Django

Variable

15 to 40

Java HikariCP

10

20 to 50

PgBouncer transaction mode

Variable

Sized for concurrent transactions

The core argument

Framework defaults for connection pooling exist because the framework cannot know your workload. The default is a reasonable starting point for a generic small application. Your application is not generic. Your application is specific to its concurrency, its query patterns, and its database capacity. The default is the starting point. Tuning is the work.

The teams that run on defaults discover the problem when traffic grows. The pool that worked at one hundred concurrent users does not work at one thousand. The application stalls under load. The database is fine. The pool is the bottleneck. The team scales the database. The bottleneck remains because it was never the database. The team is confused and the customers are unhappy.

The right approach is to size the pool deliberately. Measure the concurrency at peak load. Pick a pool size that handles the 99th percentile of concurrency. Verify the total across servers fits under the database's max_connections divided by two. Reserve the other half for migrations, admin queries, and one off operations.

The other piece is the queue behavior. When the pool is full and new requests arrive, what happens. Wait. Time out. Reject. The default is usually wait forever, which produces hung requests. The right behavior is to wait a few seconds then time out, so the failure surfaces quickly rather than appearing as a slow response.

The tuning workflow

Step

Detail

Measure concurrency

Database connections in use at peak load

Estimate growth

Where will concurrency be in six months

Compute pool size

Concurrency at growth peak plus small buffer

Verify database capacity

Total pool across servers under max_connections / 2

Set acquisition timeout

A few seconds

Set connection lifetime

30 minutes to a few hours

Add monitoring

Checkout time, queue depth, errors

Test under load

Confirm the new size handles peak

How much does this cost

The cost of tuning is engineering time. A few hours to measure and adjust. The infrastructure cost difference is negligible. The savings show up as fewer incidents and lower latency under load.

Features the pool tuning must have

  • A measurement of current concurrency.
  • A pool size set deliberately based on the measurement.
  • An acquisition timeout that surfaces failures quickly.
  • A connection lifetime that prevents stale connections.
  • Monitoring on the three key metrics.
  • An alarm on saturation.
  • A documented review cadence as the workload changes.

Expert opinion

Most connection pools running in production today have never been tuned, which means most of them are a traffic spike away from an incident nobody can explain. Measuring concurrency and sizing the pool deliberately costs an afternoon. Skipping that step costs a war room and a few days of finger pointing at the database. The discipline is one of the highest leverage observability practices in any production stack, and almost nobody budgets time for it until it is too late.

Yashveer Singh, founder of Yashveer Labs

How this played out on a real project

A client SaaS was running on the Node.js pg library default pool of 10. They had four application servers. Their Postgres allowed 100 connections. At peak load they were experiencing latency that they had attributed to slow queries.

The pool checkout time at peak was 600 milliseconds. The slow queries were actually fine. The pool was undersized.

We measured concurrency. The peak was around 30 concurrent operations per server. We raised the pool to 40 per server. Total connections were 160 across four servers, which was above the Postgres limit. We added PgBouncer in transaction mode with a pool of 50 to Postgres. The application could have 160 connections to PgBouncer. PgBouncer maintained 50 connections to Postgres.

The latency at peak dropped dramatically. The checkout time fell to under 5 milliseconds. The database was no longer near its connection limit. The fix took a day. The improvement was immediate.

For more on the related work, see connection pooling the quiet killer of SaaS performance and PostgreSQL performance at scale the tweaks that move the needle.

Common mistakes teams make

  1. Running on framework defaults.
  2. Sizing the pool to match worker count instead of concurrency.
  3. No measurement before tuning.
  4. No PgBouncer for serverless workloads.
  5. Acquisition timeout set to infinite.
  6. No monitoring on the pool.
  7. Pool total across servers exceeds database limit.
  8. No documented review cadence as load grows.

A 30 day plan

  1. Week one. Measure current concurrency at peak.
  2. Week two. Compute and apply the right pool size.
  3. Week three. Add PgBouncer or equivalent if needed.
  4. Week four. Add monitoring and alarms. Test under synthetic load.

For more on the related work, read connection pooling the quiet killer of SaaS performance and database query performance the five patterns that hurt the most. On the broader performance side, backend performance budgets how to set them is the natural next read.

FAQ

Frequently asked

  • What are the typical defaults?
  • How do I pick the right size?
  • What if my workload is bursty?
  • What about long running queries?
  • Should the pool be the same across services?
  • What about pgbouncer pool mode and pool size?
  • What is the worst tuning mistake?

Author

Why Yashveer Singh is the call for this work

I have spent the last four years writing software that runs in production. Three live client sites. A Roblox game with real players. Nexli, a school management system about to launch into private testing. Nyxera, a fully local AI assistant. Most people writing about this topic are summarizing other people's blog posts. I am writing from the codebase. If you want this kind of work done right, I am the person you call. Yashveer Singh, founder of Yashveer Labs.

Start the conversation See the work DM on Instagram