Why Every Application Needs Connection Pooling

Opening a fresh database connection for every request is a common performance mistake — connection establishment (TCP handshake, authentication, session setup) is genuinely expensive, and pooling amortizes that cost across many requests.

# WITHOUT pooling - a new, expensive connection for every single request
def get_user(user_id):
    conn = create_new_db_connection()  # slow, every single time
    result = conn.query(f"SELECT * FROM users WHERE id = {user_id}")
    conn.close()
    return result

# WITH pooling - a small set of connections reused across requests
pool = ConnectionPool(min_size=5, max_size=20)

def get_user(user_id):
    with pool.acquire() as conn:  # borrows an ALREADY-OPEN connection
        return conn.query("SELECT * FROM users WHERE id = %s", [user_id])

Sizing the Pool Correctly

A pool that’s too small causes requests to queue waiting for an available connection under load; too large, and you risk exceeding the database’s own max_connections limit — especially with multiple application server instances each running their own pool. A common rule of thumb: total pool size across all application instances should stay comfortably below the database’s configured connection limit, with headroom for admin/monitoring connections.

This applies to virtually any database, not just one specific technology — MySQL, PostgreSQL, MongoDB, and Redis all benefit from connection pooling for exactly the same underlying reason: connection setup cost, paid once per pooled connection instead of once per request.