Cheatsheet tracing a request end-to-end.

High-level flow

1. ASGI server (Uvicorn) accepts HTTP, parses to ASGI scope.
2. FastAPI middleware stack runs (CORS, GZip, custom).
3. Router matches path; finds handler.
4. Dependencies resolved (DI graph).
5. Pydantic validates path / query / body / headers.
6. Handler coroutine executes.
   - SQLAlchemy session acquired from pool.
   - Queries executed (potentially via repository).
   - Domain objects mutated.
   - Session committed.
7. Return value processed:
   - response_model (Pydantic) shapes / filters.
   - Serializer encodes to JSON.
8. Response middleware (reverse order).
9. ASGI server sends bytes.
10. Background tasks fire (after response sent).

Concrete example

POST /users with {"email":"[email protected]","password":"..."}:

# 1. ASGI: parses HTTP, builds scope.
# 2. Middleware: CORS, request_id, tracing.
# 3. Router: matches POST /users → create_user handler.
# 4. Deps: get_db opens a session.
# 5. Pydantic: UserCreate validated.
# 6. Handler:
async def create_user(data: UserCreate, db: AsyncSession = Depends(get_db)):
    if await db.scalar(select(User).where(User.email == data.email)):
        raise HTTPException(409, "email taken")
    user = User(email=data.email, hashed_password=hash_pw(data.password))
    db.add(user)
    await db.commit()
    await db.refresh(user)
    return user
# 7. response_model=UserRead → Pydantic shapes:
#    - reads via from_attributes
#    - omits hashed_password
#    - serializes to JSON
# 8. Middleware exit (request_id header set, trace finished).
# 9. ASGI writes bytes.

Each layer’s responsibility

LayerWhat it does
ASGIHTTP bytes ↔ Python objects
FastAPI middlewareCross-cutting (CORS, auth, logging)
RouterPath → handler matching
DependsPer-request resources (db, user, settings)
Pydantic (in)Type / shape validation; constraints
HandlerBusiness logic
SQLAlchemyPersistence; queries
Pydantic (out)response_model shaping; serialization

Where things go wrong

SymptomLikely layer
422 with detailPydantic input validation
500 internal errorHandler or DB
Slow responseDB (N+1, missing index)
Wrong fields in responseMissing response_model or wrong shape
Connection refused on DBPool exhausted; PgBouncer; pool_pre_ping
Stale dataSession expire / replica lag

Concurrency

Per request:

  • One coroutine.
  • One DB session (one pool connection while in use).
  • Multiple concurrent requests share the pool.

For parallel work within a request: TaskGroup; separate sessions.

async with asyncio.TaskGroup() as tg:
    async with AsyncSessionLocal() as s1, AsyncSessionLocal() as s2:
        a = tg.create_task(s1.scalar(...))
        b = tg.create_task(s2.scalar(...))

Authentication / authorization

Token from Authorization header
  → oauth2 dep extracts string
  → current_user dep validates + queries User
  → handler receives User instance

Each layer can reject (401, 403).

Background tasks

@app.post("/signup")
async def signup(data: UserIn, bg: BackgroundTasks, db = Depends(get_db)):
    user = await create_user(db, data)
    bg.add_task(send_welcome_email, user.email)
    return user

send_welcome_email runs after response is sent (same process).

Error path

try:
    # handler logic
except IntegrityError as e:
    await db.rollback()
    raise HTTPException(409, "conflict")
except ValueError as e:
    raise HTTPException(400, str(e))

Pydantic ValidationError caught by FastAPI’s RequestValidationError handler → 422 (or custom 400).

Session boundaries

async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()       # auto-commit on success
        except Exception:
            await session.rollback()
            raise

Or explicit commit in handlers.

Cross-request lifespan resources

@asynccontextmanager
async def lifespan(app):
    app.state.engine = create_async_engine(...)
    app.state.http = httpx.AsyncClient(...)
    app.state.redis = await aioredis.from_url(...)
    yield
    await app.state.engine.dispose()
    await app.state.http.aclose()
    await app.state.redis.close()

App-wide; per-request via request.app.state.*.

Read this next

If you want a sequence diagram of this flow, 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 .