Connection Pooling: The Quiet Killer of SaaS Performance
Connection pooling is the technique of reusing database connections across requests instead of opening a new connection for each. Done well, it dramatically reduces overhead and supports many more concurrent users. Done poorly, it produces incidents that look like database performance problems but are actually pool exhaustion. The pool size, the connection lifetime, and the queue behavior are the three settings that matter most.
Written by Yashveer Singh, founder of Yashveer Labs.
What you actually need to know
- Pool exhaustion produces incidents that look like database problems.
- Total connections across all servers should fit comfortably under Postgres max_connections.
- PgBouncer is the standard for serverless Postgres workloads.
- Transaction pooling mode is the right default.
- Monitor checkout time and queue depth as the warning signals.
| Setting | Typical value |
|---|---|
| Postgres max_connections | 100 to 500 depending on instance size |
| App server pool size | max_connections / app servers / 2 |
| PgBouncer pool size | Matches app demand |
| Pool mode | Transaction for most cases |
| Connection lifetime | 30 minutes to a few hours |
| Idle timeout | Several minutes |
| Checkout timeout | A few seconds |
The core argument
Connection pool incidents are some of the most common production issues in SaaS and some of the hardest to diagnose without context. The pattern is consistent. The application gets busy. Requests slow down. The team looks at the database. The database CPU is fine. The query latency is fine. The team is confused. The actual problem is that the application has run out of connections in its pool and new requests are waiting.
The fix is sizing the pool correctly and adding visibility into pool health. The pool should be large enough to handle the application's concurrency. The total connections across all application servers should fit comfortably under the database's max_connections. The monitoring should expose pool checkout time and queue depth.
The mistake teams make is using the framework default pool size and never measuring. The default is rarely right for the application's actual load. A pool of 10 is too small for an app server that handles 200 concurrent requests. A pool of 200 is too large if the database only allows 100 connections and you have five app servers.
Serverless workloads compound the problem. Each serverless instance opens its own connection. Without a pooler in front of the database, the database hits its connection limit immediately. PgBouncer or an equivalent connection pooler is required. The pooler maintains a small pool of real connections and serves many more application connections from that pool.
The configuration that works
| Component | Setting |
|---|---|
| Application pool size | Sized for concurrency. Usually 5 to 50 per server |
| Pool acquisition timeout | A few seconds, not unlimited |
| Connection idle timeout | A few minutes |
| Connection lifetime | 30 minutes to a few hours |
| PgBouncer pool mode | Transaction |
| PgBouncer pool size to Postgres | Sized for total concurrent transactions |
| Monitoring | Checkout time, queue depth, error rate |
How much does this cost
| Approach | Cost |
|---|---|
| Default pool with no pooler | Free, breaks at scale |
| Sized pool, no pooler | Free, works to moderate scale |
| Sized pool plus PgBouncer | Modest infrastructure cost |
| Sized pool plus managed pooler (RDS Proxy, Supabase pooler) | Slightly higher cost, lower ops |
Features the connection setup must have
- Pool size set deliberately, not framework default.
- Acquisition timeout set so requests fail rather than hang forever.
- Connection lifetime set to prevent stale connections.
- Monitoring on checkout time, queue depth, and errors.
- A pooler in front of the database for serverless workloads.
- A documented connection budget across services.
- An alarm on pool saturation.
Expert opinion
Connection pooling is one of those topics that engineers do not think about until they have an incident. The teams that have had the incident never forget. The teams that have not had the incident often have pool settings that will cause one eventually. The right sizing is mechanical. The monitoring is cheap. The operational pain it prevents is real.
>
Yashveer Singh, founder of Yashveer Labs
How this played out on a real project
A client SaaS was experiencing intermittent latency spikes that the team had attributed to database performance. They had scaled up the database instance twice without improvement. The founder asked me to look.
The metrics showed pool checkout times of 800 milliseconds during the spikes. The database itself was idle. The pool was undersized for the load. The application had a pool size of 10 per server and was running 25 concurrent requests on most servers. Requests were queueing for connections.
We raised the pool size to 40 per server and added PgBouncer in front of the database to manage the total connection count. The latency spikes disappeared. The database scaling that had been done was unnecessary. The team adopted pool monitoring as a standard observability metric.
For more on the related work, see connection pooling why defaults are wrong for most stacks and PostgreSQL performance at scale the tweaks that move the needle.
Common mistakes teams make
- Using the framework default pool size.
- No pooler in front of Postgres for serverless workloads.
- Pool size larger than the database can handle.
- No monitoring on checkout time.
- No acquisition timeout. Requests hang forever.
- Session pool mode where transaction would have worked.
- Treating database performance as the problem when the pool is exhausted.
- No connection budget across services.
A 30 day plan to fix connection pooling
- Week one. Measure current pool checkout time and queue depth.
- Week two. Size the pool correctly for the concurrency.
- Week three. Add PgBouncer or equivalent if serverless.
- Week four. Wire monitoring. Set alarms on saturation.
For more on the related work, read connection pooling why defaults are wrong for most stacks and PostgreSQL performance at scale the tweaks that move the needle. On the broader scaling side, scaling from one thousand to one hundred thousand users the invisible database bottlenecks is the natural next read.
Frequently asked
Why this work lands with me
I am Yashveer Singh. Founder of Yashveer Labs. I take this kind of project because I have done enough of them to know what kills them. The version of me that writes a post like this is the same one who builds the system afterward. There is no handoff to a junior, no agency middleman, no surprise scope. That is the bet I am making on my own brand.
Posts that line up with this one.
- SaaS Architecture and Scaling
Soft Locks vs Hard Locks: A Database Concurrency Primer
Concurrency bugs are among the hardest to reproduce and the most expensive to fix. Here is how locking actually works.
- SaaS Architecture and Scaling
Database Migrations at Scale: How to Move Fast Without Breaking Things
Database migrations get harder as the table gets larger. The patterns that worked at one million rows produce locks and outages at one hundred million. Here is the playbook that keeps the migration safe.
- SaaS Architecture and Scaling
Sharding Strategies for SaaS: When to Start and When to Stop Avoiding It
Sharding is a last resort, not a first move. Here is the honest decision framework for SaaS teams.
- SaaS Architecture and Scaling
Soft Deletes vs Hard Deletes: A SaaS Data Strategy Debate
Soft deletes seem safe but create long-term complexity. Here is how to decide and what you are trading away.