Sessions and expire_on_commit

RoadmapsPython Backend

Scenario

You query a User from the database and return it from your FastAPI endpoint. It works locally. But when the frontend requests the User's posts, your app crashes with a MissingGreenlet exception.

Mental model

A database session is like a shopping cart. Calling commit() is checking out. By default, SQLAlchemy empties the cart (expires objects) after checkout, meaning any further look at the items requires a new trip to the store.

When a transaction ends, SQLAlchemy expires the objects associated with it. Reading an attribute on an expired object triggers an automatic background query to fetch fresh data. In asynchronous code, this invisible query crashes because it isn't awaited.

Deep dive

The AsyncSession is your workspace. You fetch objects, modify them, and call commit(). By default, SQLAlchemy sets expire_on_commit=True. This means after a commit, all objects in the session are marked as 'expired'.

Committing a session expires its objects by default.

If you try to read an attribute on an expired object, SQLAlchemy normally triggers a lazy-load (a new SELECT query) behind the scenes to refresh it. In synchronous code, this just runs a query. But in async code, you cannot run network I/O implicitly during a simple attribute access like user.name.

Attribute access is synchronous. You cannot await user.name.

Because an attribute access isn't awaited, SQLAlchemy's async engine throws a MissingGreenlet error when it tries and fails to fetch the data. The event loop cannot pause for a magic, invisible network call.

Unawaited background queries in async contexts throw MissingGreenlet.

The fix is two-part: First, set expire_on_commit=False when creating your async_sessionmaker. This tells SQLAlchemy to let you keep reading the object's data in memory after the commit, without triggering a refresh. Second, for relationships, you must explicitly load them at query time.

Disable expire_on_commit so objects remain readable in memory.

Code examples

The MissingGreenlet crash

async def get_user(session: AsyncSession, user_id: int):
    user = await session.get(User, user_id)
    await session.commit()  # object is expired here!
    
    # Crash! user.name tries to lazy-load synchronously
    return {"name": user.name}

Because the session committed with expire_on_commit=True (the default), user.name attempts a database trip. Since it's not awaited, the async engine crashes with MissingGreenlet.

The fix: expire_on_commit=False

from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine

engine = create_async_engine("postgresql+asyncpg://...")
# Fix: Disable expiry so objects remain readable after commit
async_session = async_sessionmaker(
    engine, 
    expire_on_commit=False
)

async def get_user_fixed(session: AsyncSession, user_id: int):
    user = await session.get(User, user_id)
    await session.commit() 
    return {"name": user.name}  # Works, reads from memory

By configuring the session maker with expire_on_commit=False, objects retain their state in memory after the transaction closes, preventing implicit I/O.

Key points

Common mistakes

Recall questions

Questions & answers

Your FastAPI endpoint queries a record, updates a view counter, commits the session, and then returns the record. It throws a MissingGreenlet exception when Pydantic tries to serialize the response. Why?

The commit expired the object. When Pydantic reads its attributes to serialize the response, SQLAlchemy tries to lazy-load the data synchronously, which is illegal in async code. Set expire_on_commit=False on the session.

Can we just await the attribute access, like `await user.name`, to fix MissingGreenlet?

No, Python properties and attribute access cannot be awaited natively. The data must be loaded into memory before the attribute is accessed.

Continue learning

Previous: Async SQLAlchemy and SQLModel

Next: Eager Loading with selectinload

Return to Python Backend Roadmap