FastAPI: From Basics to Production · Databases with SQLAlchemy 2.0 and Alembic

Setting Up Async SQLAlchemy

Engine, session factory, declarative models and per-request sessions.

SQLAlchemy 2.0 supports asyncio via create_async_engine (e.g., postgresql+asyncpg://) and async_sessionmaker. Define models with typed Mapped[...] annotations and mapped_column. Use expire_on_commit=False so objects remain usable after commit in async code, configure pool size per worker, and create one session per request via a yield dependency.

from datetime import datetime
from sqlalchemy import ForeignKey, String, func
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

engine = create_async_engine(settings.database_url, pool_size=10, max_overflow=5, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    email: Mapped[str] = mapped_column(String(320), unique=True, index=True)
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())
    orders: Mapped[list["Order"]] = relationship(back_populates="user")


class Order(Base):
    __tablename__ = "orders"
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
    total_cents: Mapped[int]
    user: Mapped[User] = relationship(back_populates="orders")

Common pitfalls

  • Sharing one AsyncSession across concurrent tasks — sessions aren't concurrency-safe.
  • Default expire_on_commit=True causing lazy reloads (errors in async) after commit.
  • Pool size × workers exceeding the database's max connections.

Key points

  • create_async_engine + async_sessionmaker
  • Typed Mapped models
  • One session per request/task

Further reading: SQLAlchemy: asyncio extension

Practice questions

Why set expire_on_commit=False for async sessions?

Answer

With expiry, attributes are reloaded on next access after commit, which requires implicit I/O that async sessions don't allow (MissingGreenlet errors).

How do you size the connection pool?

Answer

Consider pool_size + max_overflow per process times the number of processes; keep the total below the database's connection limit (or use PgBouncer).

Open in the app to mark lessons complete, take notes and track your progress.