Key takeaways
- A pool should be small. HikariCP’s pool-sizing guide starts near (CPU cores x 2) + effective spindle count and calls a pool of 10,000 for 10,000 users “shear insanity”.
- The ceiling is the database: instances x pool size (including every autoscaled instance and every background worker) must stay under the database’s
max_connectionsminus its reserved connections. - Defaults differ a lot: HikariCP 10, SQLAlchemy 5 plus 10 overflow, node-postgres 10, PgBouncer 20 server connections per user and database pair. Read yours before a load test.
- A load test finds the real answer: raise virtual users in steps and watch pool wait time, database connections, throughput and p95 together.
To optimize a database connection pool, size it from the database’s limits and CPU rather than from the number of users, set a short wait timeout so overload fails fast, and then run a stepped load test to confirm where throughput stops growing. This post gives the sizing arithmetic, the documented defaults of the common pools and poolers, and a test plan. It replaces an earlier version of this page that carried invented figures; everything numeric below is either from a linked source or recomputed in the text.
What does a connection pool do, and what goes wrong when it is misconfigured?
Opening a database connection costs a network round trip, authentication and, on PostgreSQL, a new server process, so applications keep a set of open connections and lend them out per request. That is the pool. Two things go wrong:
- Too small: requests wait for a free connection. Throughput flattens while CPU on the app and on the database stays low, and response time climbs because requests queue.
- Too large (or too many pools): the database is asked to run more queries at once than it has cores and disks to serve, so every query slows down, or new connections are refused. PostgreSQL’s documentation notes that it sizes shared memory and other resources directly from
max_connections, and MySQL answers with “Too many connections” when all permitted connections are in use (postgresql.org and dev.mysql.com, read 2026-10-23).
The first failure is easy to see in a load test. The second often appears only when the application scales out, because each new instance brings its own pool.
How do you size a database connection pool?
Start from what the database can run at once, not from the number of users. The HikariCP project’s “About Pool Sizing” page gives a starting point of roughly ((core_count x 2) + effective_spindle_count) active connections, and works one example: a 4-core server with one hard disk gives 9, which it rounds to 10. It reports an Oracle Real-World Performance demonstration in which cutting a pool from 2,048 connections to 96 reduced response times from about 100 ms to about 2 ms, and it says that a pool of 10,000 for 10,000 front-end users “would be shear insanity” (github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing, read 2026-10-23). Treat the formula as a place to start the test, not a result: it was written for a database server’s own cores and disks, and your workload may differ.
Two details from the same page matter in practice. Threads that need more than one connection at a time can deadlock a small pool; the minimum to avoid it is Tn x (Cm - 1) + 1, where Tn is the maximum number of threads and Cm the most connections one thread holds at once (3 threads holding 4 each gives 3 x 3 + 1 = 10). And a pool is also a throughput limit: with N connections each held for t seconds per request, the most the pool can serve is N / t requests per second. A pool of 10 with a 20 ms hold time tops out at 10 / 0.020 = 500 requests per second whatever the CPU is doing. The post on raising API throughput works that arithmetic with Little’s Law, so it is not repeated here.
How many connections can each instance have?
Divide what the database can spare by the number of instances you may run at peak. A worked example (an illustration, not a measurement): PostgreSQL’s max_connections is typically 100 by default and superuser_reserved_connections defaults to 3, so 97 are available to ordinary roles. Keep 10 for migrations, monitoring and admin sessions: 87 remain. With 6 instances that is 87 / 6 = 14.5, so a maximum pool of 14 per instance. If autoscaling can reach 8 instances, 87 / 8 = 10.9, so the safe maximum is 10. A pool set to 14 would then ask for 8 x 14 = 112 connections against a limit of 97. Count worker processes, cron jobs and read replicas’ own pools the same way, since each opens connections of its own.
PostgreSQL’s max_connections can only be changed at server start, and a standby must be set to the same value or higher than the primary (postgresql.org/docs/current/runtime-config-connection.html, read 2026-10-23). MySQL’s default is 151 and the server permits one extra connection for accounts with the CONNECTION_ADMIN (or SUPER) privilege, so an administrator can still log in when the limit is hit (dev.mysql.com/doc/refman/8.4, read 2026-10-23).
What are the default pool settings?
Defaults, as documented on 2026-10-23, so you know what a load test is really measuring:
| Pool | Max size | Wait for a connection | Idle and lifetime |
|---|---|---|---|
| HikariCP (Java) | maximumPoolSize 10 | connectionTimeout 30 s | idleTimeout 10 min, maxLifetime 30 min, keepaliveTime 2 min |
| SQLAlchemy QueuePool (Python) | pool_size 5 plus max_overflow 10 | timeout 30 s | pool_recycle off by default (-1) |
| node-postgres Pool (Node.js) | max 10 | connectionTimeoutMillis 0, which means no timeout | idleTimeoutMillis 10 s |
| PgBouncer (server side) | default_pool_size 20 per user and database pair, max_client_conn 100 | query_wait_timeout 120 s | server_idle_timeout 10 min, server_lifetime 1 hour |
Sources: the HikariCP README, the SQLAlchemy pooling docs, the node-postgres pool API page and the PgBouncer configuration reference. Two of these deserve a decision before production. node-postgres waits forever by default when the pool is exhausted, so a saturated pool shows up as requests that never finish rather than as errors; set connectionTimeoutMillis to a value below your client timeout. And HikariCP’s own guidance is to set maxLifetime several seconds shorter than any connection time limit imposed by the database or infrastructure, so a connection is retired by the pool before something else cuts it.
When do you need PgBouncer or RDS Proxy?
An external pooler helps when many clients each hold a connection that is idle most of the time, which is typical of many app instances, serverless functions, or frameworks that open a connection per process. It lets thousands of client connections share a small number of server connections. Amazon’s guidance lists databases that hit “too many connections” errors and AWS Lambda functions that make frequent short connections as good candidates for RDS Proxy (docs.aws.amazon.com RDS Proxy planning, read 2026-10-23).
The trade-off is that sharing a server connection between clients breaks anything that depends on session state. PgBouncer’s feature table shows that in transaction pooling mode SET/RESET, LISTEN, PREPARE/DEALLOCATE and session-level advisory locks are not supported, while protocol-level prepared statements work once max_prepared_statements is set to a non-zero value (pgbouncer.org/features.html, read 2026-10-23). RDS Proxy handles session state by pinning: after a session variable change it ties the client to one database connection until the session ends, and the documentation advises reducing pinning if most of your connections hit it (docs.aws.amazon.com RDS Proxy, “Avoiding pinning”). Load test through the pooler you will run in production, not around it.
How do you load test a connection pool?
The aim is to find the load at which the pool, not the database or the code, becomes the limit, and to check that overload fails quickly and cleanly. A plan that works:
- Record the settings first: maximum pool size, wait timeout, idle timeout and lifetime in every service that talks to the database, plus the database’s connection limit and how many instances can run at peak.
- Ramp users in steps (for example every few minutes) instead of jumping to the target, and hold each step long enough to settle. A step chart shows the exact user count where the curve bends. If you are unsure how many users map to a given request rate, calculating concurrent users turns requests per second and response time into a user count.
- Watch four signals together: throughput, p95 response time, pool wait time and active connections from the application’s pool metrics, and the number of connections on the database (
pg_stat_activityfor PostgreSQL,Threads_connectedandMax_used_connectionsfor MySQL). - Read the pattern. Throughput flat, p95 rising, pool active connections at the maximum and database CPU low points at the pool (or at slow queries holding connections too long). “Too many connections” errors, or database CPU at its limit while the pool has room, point at the database side. Errors that appear only at the highest step usually mean the wait timeout is shorter than the queue.
- Change one setting, rerun the same test, and compare the runs. A bigger pool that raises throughput and leaves p95 flat was too small; a bigger pool that raises p95 was already large enough.
- Test the failure case on purpose: set the load above the capacity you found and check that requests fail within your timeout, not after minutes.
A LoadFocus load test covers the outside view of this plan: you set virtual users, duration, ramp-up time and ramp-up steps, add the requests (GET, POST, PUT, PATCH, DELETE with headers, query parameters and a body), and read response time percentiles, errors and throughput per request. Pass/fail thresholds (maximum p95 or p99, maximum error rate, minimum throughput) grade each run, so a pool change that makes things worse shows up as a red run (loadfocus.com/load-testing and the docs pages “How to Run a New Load Test” and “Pass/Fail Thresholds”, read 2026-10-23). LoadFocus does not see inside your application: pool wait time and database connection counts come from your own metrics, so put them on a dashboard next to the test. To design the traffic itself, see how to design load testing scenarios.
Which common mistakes make a pool look healthy when it is not?
- Testing from one instance: a pool of 20 on one instance looks fine, and the same setting on 12 instances asks for 240 connections.
- Fixed test data: every virtual user fetching the same row stays in the database cache and holds a connection for a shorter time than real traffic does.
- No think time or a constant rate: real traffic comes in bursts, which is what drains a pool. Add a ramp and a spike step.
- Long transactions: holding a connection while calling another service or waiting on user input multiplies the hold time
tin N / t. Release the connection before the external call. - Leaks: a connection that is never returned shrinks the pool a little on every request. HikariCP’s
leakDetectionThreshold(off by default) logs a connection held longer than a limit you set; a long, steady soak test shows a leak as active connections that only climb.
Frequently Asked Questions
What is a good database connection pool size?
Start near (CPU cores x 2) plus the effective spindle count of the database server, as the HikariCP guide suggests, share that across all instances, and adjust with a load test. For a database server with 4 cores and one disk that is about 10 connections in total, not per instance.
Does a bigger connection pool make an application faster?
Only while the database has idle capacity. Past the number of queries it can run at once, extra connections add queueing inside the database and every query gets slower. The HikariCP guide cites a case where a smaller pool cut response times from about 100 ms to about 2 ms.
How do I find out whether the pool or the database is the bottleneck?
Compare the pool’s active connection count with its maximum, and the database’s CPU and connection count with their limits. A full pool with an idle database points at the pool or at queries that hold connections too long; a busy database with a pool that has free connections points at the database.
Should I use PgBouncer or RDS Proxy?
Use one when many clients keep connections open that are mostly idle, or when functions open connections frequently, and test the application through it. Check for features that break under transaction pooling, such as session-level advisory locks and SET statements.
Why do I get “too many connections” under load?
All permitted connections are in use. Add up the pool maximums of every instance and worker, compare the total with the database limit, and lower the per-instance maximum or add a pooler.
Sources
- HikariCP, About Pool Sizing, https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing (read 2026-10-23)
- HikariCP README (configuration defaults), https://github.com/brettwooldridge/HikariCP (read 2026-10-23)
- PgBouncer configuration and features, https://www.pgbouncer.org/config.html and https://www.pgbouncer.org/features.html (read 2026-10-23)
- PostgreSQL, connection settings, https://www.postgresql.org/docs/current/runtime-config-connection.html (read 2026-10-23)
- MySQL 8.4 reference manual, max_connections and “Too many connections”, https://dev.mysql.com/doc/refman/8.4/en/too-many-connections.html (read 2026-10-23)
- SQLAlchemy, connection pooling, https://docs.sqlalchemy.org/en/20/core/pooling.html (read 2026-10-23)
- node-postgres, Pool API, https://node-postgres.com/apis/pool (read 2026-10-23)
- Amazon RDS Proxy, planning and pinning, https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-planning.html and https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-pinning.html (read 2026-10-23)