Skip to content
AITroveRead. Build. Understand.
Make this comfortable

FastAPI Async Sessions and Transaction Ownership

Last updated: 5 Oct 20268 min read
tutorial
IntermediateBy AITrove Editorial

A database session is a unit of interaction with a database connection pool, not a global application object. A FastAPI dependency can create it and close it when its lifetime ends. The command service decides when to begin, commit, and roll back a transaction; cleanup after a response is too late to be the only definition of success. Async syntax also does not turn a synchronous database driver into nonblocking work. An async route should use an async database session and driver, or remain synchronous with a synchronous session. A returned ORM object can hold lazy relationships that try to query after the session has closed or while a response is being serialized.

Working case

Reviewer 47 approves case 62. The first implementation shares a session object across requests, so a rollback triggered by reviewer 81's failed action contaminates unrelated work. A second implementation commits in dependency teardown after returning a success response; a constraint failure occurs after the client has already seen success. The repaired API creates a session for each request, asks a service to open one transaction, verifies assignment and revision, inserts the approval and outbox row, and commits before building the response. The route returns plain values. A concurrent replay uses a unique operation key and recovers the existing result without a second transition.

Implementation boundary

python
from collections.abc import AsyncIterator
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from .database import session_factory
from .services import ApprovalCommand, ApprovalOutcome, Reviewer, approve_once, current_reviewer

router = APIRouter()


async def get_session() -> AsyncIterator[AsyncSession]:
    async with session_factory() as session:
        yield session


@router.post("/permits/{case_id}/approval")
async def approve_permit(
    case_id: int,
    command: ApprovalCommand,
    reviewer: Annotated[Reviewer, Depends(current_reviewer)],
    session: Annotated[AsyncSession, Depends(get_session)],
) -> dict[str, int | str]:
    outcome: ApprovalOutcome = await approve_once(
        session, reviewer.id, case_id, command.operation_id, command.expected_revision
    )
    return {"case_id": outcome.case_id, "status": outcome.status}

Create an async session factory at application setup and yield one session per request through a dependency. Close it in an async context manager. In the approval service, use an explicit transaction block and keep the permission query, status check, operation insert, and outbox insert inside it. Enforce operation uniqueness at the database layer because a pre-insert existence query alone races. Return a materialized DTO after the transaction commits. Never pass a live session into a background task; that task should create its own session. If the response streams from a database cursor, choose a dependency lifetime that actually covers the stream or detach the data before sending. Test the installed framework version's dependency cleanup timing.

Cost and boundaries

One session per request can borrow one pooled connection when it executes SQL; pool capacity, transaction duration, and query plan determine throughput. A session dependency with a long response lifetime may hold scarce resources if an endpoint streams slowly. Eagerly materializing 47 summaries consumes O(page size) memory but releases the database before network delivery. A row lock reduces conflicting writes but can queue hot cases. Retained command and outbox rows grow with accepted operations; they need explicit cleanup rules after the replay window. Track connection checkout time, open transaction age, rollback rate, duplicate key conflicts, and response serialization queries, not just endpoint latency.

Failure trace

Run two approvals concurrently with the same operation ID and assert one case transition and one outbox row. Force a constraint failure after the case update; no success response should be emitted and the whole transaction should roll back. Invoke an endpoint after another request's rollback and verify its session is independent. Return a lazy relationship in a negative test, close the session, and observe serialization failure; then return a DTO instead. Hold a streaming response open and inspect whether the dependency retains a connection. Cancel the client during an approval and determine whether the database committed; replay the stable operation ID to resolve ambiguity.

Verification

  • Every request receives its own session lifetime.
  • The command service commits before the response is constructed.
  • A unique operation key and durable outbox row share the transaction.

Practice drill

Create a permit table, reviewer assignment table, operation table with a unique key, and outbox table. Implement a request-scoped async session dependency and an approval service with one transaction. Seed case 62 for reviewer 47 only. Exercise missing permission, stale revision, duplicate operation, and a forced failure after case update. Convert the service outcome into a plain public response before returning. Add a 47-row queue read and measure pool checkout and query count under concurrent requests. Compare the behavior of a fast JSON response and a deliberately slow stream before deciding where session cleanup belongs.

Decision note

Own commit and rollback inside the command service; let the dependency own only request-scoped session cleanup.

Common Mistakes

  • Sharing one mutable session between requests.
  • Committing only in dependency teardown after a success response.
  • Passing a request session into work that runs after the response.

Related lessons

FastAPI Contract and Resource Boundaries; FastAPI Dependencies and Object Authorization; FastAPI Input, Output, and Error Contracts; FastAPI Lifespan, Background Work, and Delivery State; Data Persistence; Django Forms, CSRF, Atomic Approval, and On-Commit Work.

Apply and check

Build Project: FastAPI permit service boundaries and review Web Development: FastAPI contract and resource quiz.

web-tech
web-development
Storage details