Connection pooling and engine tuning

SQLAlchemy · Advanced · 12 min

Pool settings decide whether your app survives database restarts, idle timeouts and traffic spikes.

An Engine keeps a pool of DBAPI connections so each request doesn't pay to connect. The default for most databases is QueuePool:

  • pool_size=5: connections kept open.
  • max_overflow=10: extra connections allowed under load, closed when returned.
  • pool_timeout=30: seconds to wait for a free connection before raising TimeoutError, which usually means sessions are being held too long or leaked.
  • pool_pre_ping=True: test a connection with a lightweight ping on checkout and replace it if the server dropped it (after a restart or failover). Cheap insurance; recommended.
  • pool_recycle=1800: replace connections older than this many seconds, for servers or proxies that kill idle connections (MySQL's wait_timeout, cloud load balancers).

Size the pool against the database's connection limit: (pool_size + max_overflow) x processes must fit. With many processes or serverless functions, use an external pooler like PgBouncer and poolclass=NullPool in the app.

Forking: connections must not cross process boundaries. With multiprocessing or pre-fork servers (Gunicorn, Celery prefork), create the engine after the fork, or call engine.dispose(close=False) in the child so it starts with a fresh pool.

engine.pool.status() shows checked-in and checked-out counts, handy when debugging exhaustion.

Example

from sqlalchemy import create_engine, text
from sqlalchemy.pool import QueuePool

engine = create_engine(
    "sqlite:///app.db",
    poolclass=QueuePool,   # explicit here; the default for server databases
    pool_size=5,
    max_overflow=10,
    pool_timeout=30,
    pool_pre_ping=True,    # replace dead connections transparently
    pool_recycle=1800,     # recycle connections older than 30 minutes
)
with engine.connect() as conn:
    conn.execute(text("SELECT 1"))
    print(engine.pool.checkedout())   # 1
print(engine.pool.checkedout())       # 0: returned to the pool
print(engine.pool.status())
# Pool size: 5  Connections in pool: 1 Current Overflow: -4 Current Checked out connections: 0

Remember

  • pool_size + max_overflow bound concurrent connections per process
  • pool_pre_ping handles connections dropped by the server
  • pool_recycle beats idle-connection timeouts
  • After fork, dispose the inherited pool or create the engine in the child

Watch out for

  • Sharing an engine's pooled connections between a parent and forked worker processes.

Check yourself

  1. Requests hang and then fail with a pool TimeoutError, while the database is idle. Most likely cause?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sqlalchemy.org/en/20/core/pooling.html