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.