Cheatsheet for the engine/connection layer. Long-form: Textbook Ch 2 .

URL formats

# Postgres
"postgresql+asyncpg://user:pass@host/db"      # async (recommended)
"postgresql+psycopg://user:pass@host/db"      # psycopg v3 (sync or async)
"postgresql+psycopg2://user:pass@host/db"     # legacy sync

# MySQL
"mysql+aiomysql://user:pass@host/db"
"mysql+asyncmy://user:pass@host/db"
"mysql+pymysql://user:pass@host/db"

# SQLite
"sqlite:///app.db"
"sqlite+aiosqlite:///app.db"
"sqlite:///:memory:"

Create engine

from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import create_async_engine

engine = create_engine(URL, echo=False)
engine = create_async_engine(URL, echo=False)

Pool params

engine = create_async_engine(
    URL,
    pool_size=20,            # persistent connections
    max_overflow=10,         # extra under load
    pool_timeout=30,         # wait this long for free conn
    pool_pre_ping=True,      # ping on checkout (catch dead conns)
    pool_recycle=3600,       # recycle conns > N seconds
    echo=False,
    echo_pool=False,
    future=True,
)

Pool classes

from sqlalchemy.pool import NullPool, StaticPool, QueuePool

# No pool (serverless)
engine = create_engine(URL, poolclass=NullPool)

# Single shared connection (SQLite in-memory tests)
engine = create_engine(
    "sqlite://",
    poolclass=StaticPool,
    connect_args={"check_same_thread": False},
)

Async default is AsyncAdaptedQueuePool.

connect_args

# asyncpg
engine = create_async_engine(URL, connect_args={
    "server_settings": {
        "application_name": "myapp",
        "search_path": "myapp,public",
        "timezone": "UTC",
    },
    "command_timeout": 60,
    "statement_cache_size": 1000,
})

# psycopg v3
engine = create_async_engine(URL, connect_args={
    "options": "-c search_path=myapp,public -c application_name=myapp",
})

Connection usage

# Sync
with engine.connect() as conn:
    result = conn.execute(text("SELECT 1"))
    print(result.scalar())

with engine.begin() as conn:    # auto-commit on exit
    conn.execute(text("CREATE TABLE ..."))

# Async
async with engine.connect() as conn:
    result = await conn.execute(text("SELECT 1"))

async with engine.begin() as conn:
    await conn.execute(text("..."))

Pool stats

print(engine.pool.status())
print(engine.pool.size(), engine.pool.checked_out())

Expose as Prometheus gauges.

Events

from sqlalchemy import event

@event.listens_for(engine.sync_engine, "connect")
def on_connect(dbapi_conn, conn_record):
    cur = dbapi_conn.cursor()
    cur.execute("SET TIME ZONE 'UTC'")
    cur.execute("SET statement_timeout = '30s'")
    cur.close()

@event.listens_for(engine.sync_engine, "checkout")
def on_checkout(dbapi_conn, conn_record, conn_proxy):
    ...

Slow query log

import time

@event.listens_for(engine.sync_engine, "before_cursor_execute")
def before(conn, cur, stmt, params, ctx, executemany):
    ctx._t = time.time()

@event.listens_for(engine.sync_engine, "after_cursor_execute")
def after(conn, cur, stmt, params, ctx, executemany):
    dur = time.time() - ctx._t
    if dur > 0.5:
        log.warning("slow_query", sql=stmt[:500], ms=dur*1000)

Lifespan with FastAPI

@asynccontextmanager
async def lifespan(app):
    app.state.engine = create_async_engine(URL, pool_size=20, pool_pre_ping=True)
    app.state.sm = async_sessionmaker(app.state.engine, expire_on_commit=False)
    yield
    await app.state.engine.dispose()

async def get_db(request: Request) -> AsyncSession:
    async with request.app.state.sm() as session:
        yield session

Multiple engines (read replicas)

write_engine = create_async_engine(WRITER_URL, pool_size=20)
read_engine = create_async_engine(
    READER_URL,
    pool_size=40,
    connect_args={"server_settings": {"default_transaction_read_only": "on"}},
)

Dispose

await engine.dispose()   # close all pooled connections

Always in lifespan shutdown.

Cross-DB notes

  • Postgres: max_connections matters; PgBouncer for scale.
  • MySQL: similar; use connection_attributes for tracing.
  • SQLite: file-based; one writer at a time; pool less relevant.

Common mistakes

  • Engine per request (should be process-singleton).
  • pool_size too large vs max_connections.
  • Missing pool_pre_ping (stale-conn errors after restart).
  • Holding sessions across long awaits (pool starvation).

Read this next

If you want my engine + lifespan + pool-metrics starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .