Sessions and expire_on_commit
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
- No implicit I/O in async: You cannot do network calls during standard attribute access (like user.name).
- MissingGreenlet = unawaited DB call: This error almost always means SQLAlchemy tried to lazy-load data in an async context.
- expire_on_commit=False: The standard best practice configuration for async SQLAlchemy sessions.
Common mistakes
- Leaving expire_on_commit=True in async apps: This forces you to return raw dicts instead of ORM objects from your DB layer, or crashes your app when serialization libraries (like Pydantic) touch the object's attributes to build a JSON response.
Recall questions
- What does expire_on_commit=True do in SQLAlchemy?
- Why does lazy-loading crash async SQLAlchemy with a MissingGreenlet error?
- How do you configure a SQLAlchemy async session to prevent attribute-access crashes after a commit?
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