12.12. Connection Pooling
Table of Contents
Why Connection Pooling Matters
Every time your backend talks to a database, it needs a connection. Creating a new database connection is expensive. It usually involves:
- A TCP handshake between your app and the database server
- Authentication and authorization
- Allocating resources on the database side
If your application opens and closes a fresh connection for every request, it will:
- Be slow under load
- Put unnecessary stress on the database
- Run into connection limits quickly
Connection pooling solves this by reusing a small number of long‑lived connections instead of creating and destroying them all the time.
Core idea: A connection pool is a controlled, reusable set of open database connections that your application borrows and returns instead of creating new ones each time.
In modern backends, especially with ORMs, connection pooling is not an optional optimization. It is the normal, recommended way to talk to a production database.
What Is a Connection Pool?
A connection pool is a component that:
- Opens a configured number of database connections in advance, or on demand.
- Keeps those connections open and ready.
- Hands out a free connection when your code needs to talk to the database.
- Puts the connection back into the pool when your code is done.
You can think about it like a library:
- The library has a limited number of copies of a book.
- A reader borrows a copy, uses it, and returns it.
- Another reader can borrow the same copy later.
If every reader had to buy a new copy of the book for a 5‑minute read, this would be wasteful. That is what it looks like when you create a new database connection per request.
Basic lifecycle
- Application starts.
- Connection pool is created with some parameters, for example:
pool_size = 5max_overflow = 10- A web request arrives that needs the database:
- Pool gives a free connection if one is available
- If not available, the pool may open a new one (depending on config) or wait
- Your query runs using that connection.
- When you finish, the connection is returned to the pool.
The connection is not closed, it is reused for future requests.
Connection Pool Settings and Terminology
Different ORMs and drivers use different names, but the ideas are the same. Here are common settings using SQLAlchemy style names, since that is what most Python ORMs build upon.
| Setting | What it controls | Typical effect |
|---|---|---|
pool_size | Base number of open, reusable connections | Concurrency capacity from the pool |
max_overflow | Extra connections over pool_size allowed under load | How much the pool can temporarily grow |
pool_timeout | How long to wait for a free connection before raising error | Controls waiting vs failing fast |
pool_recycle | Max lifetime (seconds) of a connection | Helps avoid stale / dropped connections |
max_conns | Driver specific total connection limit | Hard safety cap (used in some drivers) |
pool_size
pool_size controls how many connections the pool will maintain regularly.
- If
pool_size = 5, up to 5 connections are kept open and reused. - If you have 100 concurrent requests, only 5 can use the DB at exactly the same time from the pool.
- Others may wait for a connection or cause new temporary connections to be created if allowed by
max_overflow.
max_overflow
max_overflow allows the pool to temporarily open more connections than pool_size.
- If
pool_size = 5andmax_overflow = 10, your app can open up to5 + 10 = 15connections. - The extra 10 are not kept open indefinitely. They can be closed when idle.
This helps handle traffic spikes without permanently increasing the base pool size.
pool_timeout
If all allowed connections (base plus overflow) are busy, new requests will either:
- Wait for a connection to be returned, or
- Fail with an error if they wait too long.
pool_timeout is that waiting limit. For example, pool_timeout = 30 means:
- If no connection becomes available within 30 seconds, an exception is raised.
pool_recycle
Many databases or network devices drop idle connections after some timeout, for example after 8 hours. If your application keeps connections longer than this, you can get weird errors like:
- "Server closed the connection unexpectedly"
- "Connection reset by peer"
pool_recycle forces connections to be closed and re‑created after a certain age.
For example, pool_recycle = 1800 (30 minutes) means:
- Whenever a pooled connection is older than 30 minutes, the next time it is checked out, it will be closed and replaced with a fresh one.
Rule of thumb: Set pool_recycle to a value lower than your database or network idle timeout to avoid stale connection errors.
Connection Pooling with SQLAlchemy
SQLAlchemy includes connection pooling by default. You rarely need to create a pool manually, it is created when you configure your Engine.
Basic example
from sqlalchemy import create_engine
DATABASE_URL = "postgresql+psycopg2://user:password@localhost:5432/mydb"
engine = create_engine(
DATABASE_URL,
pool_size=5, # base pool
max_overflow=10, # extra connections for spikes
pool_timeout=30, # wait up to 30 seconds
pool_recycle=1800, # recycle connections after 30 minutes
)When you do:
with engine.connect() as conn:
result = conn.execute("SELECT 1")SQLAlchemy uses the pool under the hood:
- It checks out a connection from the pool.
- Executes the query.
- Returns the connection to the pool when the context manager exits.
You do not call engine.connect() + connection.close() per request to create new database connections. Those calls use pooled connections.
Using the engine with a session
With the ORM, you usually work through a Session object:
from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()Here:
SessionLocal()checks out a connection from the pool when the session really needs it.db.close()returns the connection back to the pool. It does not close the pool or the engine.
This pattern is common in FastAPI dependencies and similar frameworks.
Common Pooling Patterns in Web Applications
One engine per process
A typical backend process will:
- Create exactly one global
engineobject at startup. - Use that
engineto create session objects per request.
For example, in a FastAPI style application:
# db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+psycopg2://user:password@localhost/db"
engine = create_engine(
DATABASE_URL,
pool_size=5,
max_overflow=5,
pool_timeout=30,
pool_recycle=1800,
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Each web request gets its own SessionLocal() instance, but all of them share the same underlying connection pool.
One pool per process, many processes
In production, you often run multiple worker processes.
For example:
- Gunicorn with 4 workers.
- Each worker is a separate Python process.
- Each process creates its own engine and therefore its own pool.
If pool_size = 5 and max_overflow = 5:
- Each worker process can use up to 10 connections.
- Total possible connections from your app to the database =
4 workers * 10 = 40connections.
You must consider this when configuring the database maximum connections.
Synchronous vs Asynchronous Connection Pooling
In modern Python backends you may have:
- Synchronous code with normal SQLAlchemy engines
- Asynchronous code using async drivers (like
asyncpg) and async SQLAlchemy
Pooling still exists in both cases, but the implementation details differ.
Sync engine example
from sqlalchemy import create_engine
engine = create_engine(
DATABASE_URL,
pool_size=5,
max_overflow=5,
)All database operations block the current thread until a response is received. The pool manages TCP connections that are used by synchronous DB calls.
Async engine example
from sqlalchemy.ext.asyncio import create_async_engine
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/db"
async_engine = create_async_engine(
DATABASE_URL,
pool_size=5,
max_overflow=5,
)
from sqlalchemy.ext.asyncio import async_sessionmaker
AsyncSessionLocal = async_sessionmaker(
bind=async_engine,
expire_on_commit=False,
)
async def get_db():
async with AsyncSessionLocal() as session:
yield sessionHere:
- Connections are managed by an async pool in the async driver.
- Operations are
await‑able and can run concurrently in the event loop. - The concepts
pool_size,max_overflow, andpool_recyclestill apply.
Connection Pool Tuning
Choosing pool settings is about balancing:
- Database capacity
- Application concurrency
- Resource usage
There is no single best number, but there are practical guidelines.
Basic rules of thumb
- Start small, then measure.
For many small applications you can start with: pool_sizebetween 5 and 10max_overflowbetween 5 and 10- Stay under the database limit.
Suppose: - PostgreSQL
max_connections = 100 - You will have 4 application processes
- Other services need some connections too, for example background workers
You might choose:
- Application workers:
pool_size = 5,max_overflow = 5 - So each process can use up to 10 connections
- Total maximum =
4 * 10 = 40, leaving lots of room for other components
- Set
pool_recycle.
Use a value lower than any known idle timeout in: - Database configuration
- Load balancers
- Cloud networking
If you are unsure, 30 minutes to 1 hour is often safe.
- Avoid giant pools.
A pool with 100 connections in one process often means: - The database is overloaded by idle or competing connections
- You are hiding a deeper performance or design problem
It can be better to tune queries and add caches than to keep growing the pool.
Example scenarios
Low traffic development environment
- 1 worker process
- Database on the same machine
Suggested config:
engine = create_engine(
DATABASE_URL,
pool_size=3,
max_overflow=2,
pool_timeout=30,
pool_recycle=1800,
)Medium traffic production API
- 4 worker processes (for example Gunicorn with 4 workers)
- PostgreSQL on its own small server
- PostgreSQL
max_connections = 100
Suggested config per process:
engine = create_engine(
DATABASE_URL,
pool_size=5,
max_overflow=5,
pool_timeout=30,
pool_recycle=1800,
)Maximum app connections:
4 processes * (5 + 5) = 40 connections
Common Problems and How to Avoid Them
Problem 1: "Too many connections" errors
You may see errors like:
- "FATAL: sorry, too many clients already" (PostgreSQL)
Possible reasons:
pool_sizeandmax_overfloware too high.- You run many worker processes.
- Background jobs and other services also use connections.
How to fix:
- Lower
pool_sizeormax_overflow. - Reduce the number of concurrent application processes.
- Check and tune connection pools in all services, not just one.
Problem 2: Stale or dropped connections
Symptoms:
- Random errors after your app runs for a few hours
- Messages about broken pipes or closed connections
Common cause:
- Database or network closes idle connections after a timeout.
- The pool tries to reuse them.
Fix:
- Set
pool_recycleto a value smaller than the idle timeout. - Some drivers also have a
pool_pre_pingoption that tests connections before using them:
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
pool_recycle=1800,
)Problem 3: Deadlocks or "database feels stuck"
If connections are never returned to the pool, the pool eventually runs out. Every new request will:
- Wait for a connection
- Eventually time out with a pool timeout error
Common causes:
- Forgetting to close sessions or connections.
- Keeping long running transactions open.
Solutions:
- Always use context managers and structured patterns. For example:
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()- Avoid holding sessions across large parts of the code. Keep them local to each request.
Important rule: Always return connections to the pool. In SQLAlchemy, that means closing sessions or connection objects when you are done.
Practical Examples
Example: API endpoint using a pooled session
Imagine a simple user repository method using SQLAlchemy ORM:
from sqlalchemy.orm import Session
from .models import User
class UserRepository:
def __init__(self, db: Session):
self.db = db
def get_user_by_id(self, user_id: int) -> User | None:
return self.db.query(User).filter(User.id == user_id).first()An endpoint in a FastAPI app might look like:
from fastapi import Depends, HTTPException
from .db import get_db
from .repositories import UserRepository
@app.get("/users/{user_id}")
def read_user(user_id: int, db: Session = Depends(get_db)):
repo = UserRepository(db)
user = repo.get_user_by_id(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return userKey connection pooling behaviors here:
get_db()gets a session fromSessionLocal, which is bound to the pooledengine.- When the request finishes,
get_db()closes the session. - The underlying connection returns to the pool, ready for the next request.
No extra code is needed to manage the pool directly. It is handled by the ORM and engine configuration.
Example: Background worker and pooling
If you use a background worker process, for example Celery, do not create a new engine for every task. Instead:
- Create one engine and session factory for the worker process.
- Use them across tasks.
# worker_db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+psycopg2://user:password@localhost/db"
engine = create_engine(
DATABASE_URL,
pool_size=3,
max_overflow=2,
pool_recycle=1800,
)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)Then in a task:
from .worker_db import SessionLocal
def send_daily_report():
db = SessionLocal()
try:
# use db to query and update data
...
db.commit()
except:
db.rollback()
raise
finally:
db.close() # important to return connection to the poolAgain, each task checks out a pooled connection and returns it when finished.
Key Takeaways
- Opening database connections is expensive. Reuse them using a connection pool.
- Most ORMs, including SQLAlchemy, include connection pooling by default.
- Learn the essential pool settings:
pool_sizemax_overflowpool_timeoutpool_recycle- Always close sessions or connections after use. This returns connections to the pool.
- Consider all processes and services when deciding pool sizes, not just a single application instance.
- Use
pool_recycleand optionallypool_pre_pingto avoid issues with dropped idle connections.
A solid understanding of connection pooling helps you design backends that scale without overwhelming your database, and it prevents many hard to debug production issues.
Views: 7
KAHIBARO