Files
prompts/docs/skills/fastapi-async-sqlalchemy-modernization/references/session.md
T
John Lancaster e78383be1f move
2026-06-18 22:06:40 -05:00

3.8 KiB

Async SQLAlchemy Session Management

Source:

Status: adopted Decision level: mandatory Applies to: api-runtime, workers, tests Last reviewed: 2026-06-17


Purpose

Define one canonical session model for FastAPI + SQLAlchemy asyncio:

  • configure one shared session factory,
  • create one AsyncSession per request or per unit-of-work,
  • never share one AsyncSession across concurrent tasks.

Scope and Non-Goals

  • In scope: session factory creation, FastAPI dependency wiring, request/task scoping, transaction demarcation.
  • Out of scope: ORM model design, query optimization strategy, schema migration tooling.

Rules

  • Create async_sessionmaker once from app-owned AsyncEngine.
  • Use a fresh AsyncSession for each request or explicit unit-of-work.
  • Do not share AsyncSession across asyncio.gather() or parallel tasks.
  • Prefer direct dependency injection over global scoped-session patterns in new code.
  • Use explicit transaction boundaries (async with session.begin():) for writes.

Canonical FastAPI Dependency Pattern

from collections.abc import AsyncIterator

from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker


def get_session_factory(request: Request) -> async_sessionmaker[AsyncSession]:
	return request.app.state.session_factory


async def get_db_session(
	session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
) -> AsyncIterator[AsyncSession]:
	async with session_factory() as session:
		yield session

Route usage:

from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

router = APIRouter()


@router.post("/items")
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
	async with session.begin():
		# write operations here
		...
	return {"status": "ok"}

Configuration Guidance

Typical session factory setup:

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

session_factory = async_sessionmaker(
	engine,
	class_=AsyncSession,
	expire_on_commit=False,
)

Notes:

  • expire_on_commit=False is commonly preferred in asyncio applications to reduce accidental post-commit reload behavior.
  • AsyncSession.refresh() is preferred over broad expiration patterns when state refresh is needed.

Concurrency Rules

  • One session per concurrent task.
  • If work fans out into parallel tasks, each task receives its own AsyncSession.
  • Pass sessions explicitly to service functions; avoid mutable global session state.

Anti-Patterns

  • A singleton/global AsyncSession reused across requests.
  • Sharing one AsyncSession across parallel tasks.
  • Hidden session creation in lower repository helpers with no caller control.
  • Mixing commit/rollback ownership across layers without a declared boundary.

Operational Checks

  • Exactly one async_sessionmaker is registered in app lifecycle.
  • Request handlers receive sessions from one canonical dependency.
  • No code path creates AsyncSession in module import side effects.
  • Background jobs and API handlers each create task-local sessions.

Testing Checks

  • Dependency override exists for test session factory.
  • Rollback behavior is verified for failed write units.
  • Parallel-task tests verify no shared AsyncSession instances.
  • Lifespan tests confirm session factory is initialized and teardown-safe.

Migration Notes

  • If current code uses global/shared sessions, fix scope first before refactoring query style.
  • If legacy sync patterns are present, keep session boundary rules stable while migrating incrementally.