← back to blog

Understanding database connection pools: why your app slows down under load

When I first deployed a web service, I didn't think about connection pools. Then load testing showed my app grinding to a halt at ~50 concurrent users. The fix was three lines of config — but understanding why took much longer.

What is a connection?

Every time your app talks to PostgreSQL, it opens a TCP connection, authenticates, and establishes a session. This takes ~50–100ms. If every HTTP request opens and closes a connection, you're spending more time on handshakes than on actual queries.

What is a pool?

A connection pool keeps a set of connections open and reuses them. When a request needs the database, it borrows a connection from the pool, uses it, then returns it. If all connections are busy, the request waits.

# SQLAlchemy pool config (Python)
engine = create_engine(
    DATABASE_URL,
    pool_size=10,        # Connections kept open
    max_overflow=20,     # Extra connections under load
    pool_timeout=30,     # Seconds to wait before error
    pool_pre_ping=True,  # Verify connection is alive
)

The math: how big should your pool be?

A common rule from HikariCP's docs: pool_size = (core_count * 2) + effective_spindle_count. For a 2-core server with SSD (spindle count = 1): pool size of 5. Most developers over-provision pools, which creates resource contention at the database level.

What I changed

After tuning the pool, my app handled 500 concurrent users on the same hardware. The bottleneck shifted from connection overhead to actual query performance — which is a much better problem to have.