backend

Fastapi Async Sqlalchemy Sessions: Complete Implementation Guide

Building high throughput web APIs requires non blocking database operations that align with Python's async ecosystem. When you pair FastAPI with asynchronous SQLAlchemy sessions, you avoid thread pool

Fastapi Async Sqlalchemy Sessions: Complete Implementation Guide
In this story

Building high-throughput web APIs requires non-blocking database operations that align with Python's async ecosystem. When you pair FastAPI with asynchronous SQLAlchemy sessions, you avoid thread-pool exhaustion and drastically increase concurrent request capacity. Most tutorials show basic setup code but fail to address real-world connection pool exhaustion, session leaks, and transaction rollback patterns under production loads.

This guide walks through configuring a production-ready asynchronous engine, managing session lifecycles correctly via dependency injection, executing parameterized queries, and handling common pitfalls like Greenlet green-spawn errors.

Setting Up the Asynchronous SQLAlchemy Engine

Traditional synchronous database drivers block the main thread while waiting for I/O responses. Async drivers, such as asyncpg for PostgreSQL, use non-blocking socket operations so the event loop can serve other clients while waiting for disk or network queries to complete.

To initialize an asynchronous engine, you use create_async_engine from sqlalchemy.ext.asyncio along with a proper connection string format.

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/production_db"

engine = create_async_engine(
    DATABASE_URL,
    echo=False,
    pool_size=20,
    max_overflow=10,
    pool_timeout=30,
    pool_recycle=1800,
)

AsyncSessionLocal = sessionmaker(
    bind=engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autocommit=False,
    autoflush=False,
)

Setting expire_on_commit=False is critical when working with async FastAPI endpoints. If left as True, SQLAlchemy tries to refresh object attributes after a commit by issuing lazy-load queries. In an async context, this triggers missing-greenlet errors because the session context has already closed.

Implementing the Dependency Injection Pattern

FastAPI relies on a robust dependency injection system. For database sessions, you need a generator function that yields an AsyncSession and ensures proper cleanup even if request processing fails.

from typing import AsyncGenerator

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

Using an async with context manager guarantees that the connection returns to the connection pool immediately when the request finishes. If you omit the cleanup block, unreleased sessions cause connection leaks that eventually crash your database server.

Handling Database Operations and Queries

With the session dependency ready, you can inject AsyncSession directly into your route handlers. SQLAlchemy 2.0 style syntax uses explicit select(), insert(), and update() constructs instead of legacy dynamic queries.

from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select

app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    stmt = select(User).where(User.id == user_id)
    result = await db.execute(stmt)
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FND,
            detail="User not found"
        )
    return {"id": user.id, "email": user.email}

When executing write operations, wrap modifications inside explicit transaction blocks or rely on the commit logic defined in your dependency generator. For complex multi-step workflows, manage transactions manually to maintain atomicity:

@app.post("/orders/", status_code=status.HTTP_201_CREATED)
async def create_order(order_data: OrderCreate, db: AsyncSession = Depends(get_db)):
    async with db.begin_nested():
        new_order = Order(user_id=order_data.user_id, total=order_data.total)
        db.add(new_order)
        await db.flush()
        
        item = OrderItem(order_id=new_order.id, product_id=order_data.product_id)
        db.add(item)
    
    return {"status": "success", "order_id": new_order.id}

Common Mistakes and How to Avoid Them

Developers transitioning from synchronous Flask or Django apps to FastAPI often run into specific async roadblocks.

Mixing Sync and Async Drivers

Never use a synchronous database driver like psycopg2 inside async FastAPI path operations. Even if you use async/await syntax in your routes, a blocking database driver stalls the entire event loop thread, destroying concurrency gains. Always verify your database URL starts with an async driver prefix like postgresql+asyncpg:// or mysql+aiomysql://.

📖 Related: this post

Lazy Loading Relationships Across Coroutines

In SQLAlchemy ORM, accessing relationship attributes (like user.addresses) triggers a lazy load query under the hood. In synchronous code, this happens transparently. In asynchronous code, lazy loading triggers an implicit I/O call without an active event loop scope, resulting in a MissingGreenlet exception.

To prevent this, use explicit joined loading or selectin loading in your queries:

from sqlalchemy.orm import selectinload

stmt = select(User).options(selectinload(User.addresses)).where(User.id == user_id)
result = await db.execute(stmt)
user = result.scalar_one()
# user.addresses is now fully loaded asynchronously

Forgetting Connection Pool Tuning

Default connection pool sizes are often too small for high-traffic microservices or too large for restricted database instances. Monitor your database max connections limit and configure pool_size and max_overflow accordingly so your API doesn't throw timeout errors during traffic spikes.

Frequently Asked Questions

Why am I getting a MissingGreenlet error with SQLAlchemy?

This happens when you attempt to lazy-load relationship attributes on a model after the async session has already closed or without explicitly loading them using .options(selectinload(...)) or .options(joinedload(...)).

Can I use Alembic migrations with async SQLAlchemy engines?

Yes, but Alembic itself runs synchronously. In your env.py file, you can configure Alembic to run migrations using a synchronous URL while your application runtime uses the async engine variant.

How do I handle database connection retries on startup?

Wrap your database connection check in an exponential backoff loop during the FastAPI startup event. This ensures your API container doesn't crash immediately if the database container takes a few extra seconds to initialize during docker-compose deployments.

💡 You might also like: this guide

Is async SQLAlchemy always faster than sync?

Async shines in I/O-bound applications with high concurrency (hundreds or thousands of simultaneous requests waiting on database responses). For CPU-bound scripts or low-traffic internal tools, synchronous code is simpler to write and offers negligible performance differences.

IC

Written by

Isabella Carter

As a veteran correspondent, Isabella Carter has reported from across the globe, bringing firsthand perspectives to international stories and local issues.

More from backend