SQLAlchemy’s async support doesn’t actually make your database queries run faster, it just stops your web server from waiting for them.

Let’s see it in action. Imagine a FastAPI app that needs to fetch user data and their recent posts.

# main.py
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy.future import select
from typing import List

# Assume these are your ORM models
from models import User, Post # You'll need to define these

app = FastAPI()

# Configure your async engine (replace with your actual DB URL)
DATABASE_URL = "postgresql+asyncpg://user:password@host/dbname"
engine = create_async_engine(DATABASE_URL, echo=True) # echo=True for debugging

# Create a configured "SessionLocal" class
AsyncSessionLocal = sessionmaker(
    bind=engine,
    class_=AsyncSession,
    expire_on_commit=False,
)

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

@app.get("/user/{user_id}")
async def read_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).filter(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user

@app.get("/user/{user_id}/posts")
async def read_user_posts(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(Post).filter(Post.user_id == user_id))
    posts = result.scalars().all()
    return posts

@app.get("/user/{user_id}/combined")
async def read_user_and_posts(user_id: int, db: AsyncSession = Depends(get_db)):
    # Imagine fetching user and posts in parallel
    user_stmt = select(User).filter(User.id == user_id)
    posts_stmt = select(Post).filter(Post.user_id == user_id)

    # This is where async shines: running queries concurrently
    user_task = db.execute(user_stmt)
    posts_task = db.execute(posts_stmt)

    user_result, posts_result = await asyncio.gather(user_task, posts_task)

    user = user_result.scalar_one_or_none()
    posts = posts_result.scalars().all()

    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    return {"user": user, "posts": posts}

# You'll also need Pydantic models for request/response validation
# and SQLAlchemy ORM models (User, Post) defined elsewhere.

The core problem this solves is the "blocking I/O" issue in traditional synchronous web frameworks. When a synchronous web server makes a database query, the entire worker process (or thread) handling that request is blocked. It can’t do anything else – not serve other requests, not process background tasks – until the database responds. This is especially painful with slow or high-latency database operations.

Async SQLAlchemy, when paired with an async web framework like FastAPI (which is built on Starlette and Uvicorn, both async-native), allows the web server to not block. When await db.execute(...) is called, instead of the worker process freezing, the event loop in Uvicorn can switch to another task. This could be another incoming web request, a background job, or any other awaitable operation. When the database query finishes and its result is ready, the event loop will resume the original task.

Internally, create_async_engine sets up connections that can be managed by an asyncio event loop. AsyncSession provides an asynchronous interface for interacting with these connections. The await db.execute() and await db.commit() calls are crucial; they signal to the event loop that this operation is I/O-bound and that control can be yielded. asyncio.gather is particularly powerful, allowing you to run multiple awaitable database operations concurrently if they don’t depend on each other, drastically reducing the total time spent waiting for I/O.

The most surprising thing most people miss is that you must use an async-compatible database driver. For PostgreSQL, this means asyncpg (which you’d install via pip install asyncpg). If you try to use psycopg2 (the synchronous driver) with create_async_engine, you’ll still block, and SQLAlchemy will raise an error or behave unpredictably. The postgresql+asyncpg:// in the DATABASE_URL tells SQLAlchemy which driver to use.

The next concept you’ll likely grapple with is managing asynchronous transactions, specifically when to await db.commit() and await db.rollback() within your API endpoints or background tasks.

Want structured learning?

Take the full Fastapi course →