[{"data":1,"prerenderedAt":1830},["ShallowReactive",2],{"page-\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Fusing-expire-on-commit-false-in-fastapi-dependencies\u002F":3},{"id":4,"title":5,"body":6,"description":1822,"extension":1823,"meta":1824,"navigation":85,"path":1826,"seo":1827,"stem":1828,"__hash__":1829},"content\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Fusing-expire-on-commit-false-in-fastapi-dependencies\u002Findex.md","Using expire_on_commit=False in FastAPI Dependencies",{"type":7,"value":8,"toc":1804},"minimark",[9,13,40,45,204,226,230,235,246,265,273,277,280,332,335,339,357,726,730,933,946,950,1140,1144,1148,1161,1164,1307,1317,1321,1327,1460,1463,1467,1477,1487,1499,1643,1647,1653,1696,1699,1703,1723,1733,1751,1770,1774,1800],[10,11,5],"h1",{"id":12},"using-expire_on_commitfalse-in-fastapi-dependencies",[14,15,16,17,21,22,25,26,29,30,33,34,39],"p",{},"Set ",[18,19,20],"code",{},"expire_on_commit=False"," in ",[18,23,24],{},"async_sessionmaker"," and your FastAPI responses will serialize ORM instances correctly after commit — without it, every attribute read after ",[18,27,28],{},"await session.commit()"," raises ",[18,31,32],{},"MissingGreenlet"," because SQLAlchemy tries a synchronous lazy reload that has nowhere to run. This page is part of the ",[35,36,38],"a",{"href":37},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002F","guide to integrating SQLAlchemy async with FastAPI and Starlette",".",[41,42,44],"h2",{"id":43},"quick-answer","Quick Answer",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-python shiki shiki-themes github-light github-dark","# WRONG — default expire_on_commit=True causes MissingGreenlet after commit\nfrom sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\")\n\n# Omitting expire_on_commit means it defaults to True — dangerous in async FastAPI\nbad_factory = async_sessionmaker(engine, class_=AsyncSession)\n\n\n# CORRECT — expire_on_commit=False keeps in-memory attribute values after commit\ngood_factory = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=False,  # This one setting prevents the post-commit MissingGreenlet\n)\n","python","",[18,53,54,63,80,87,106,111,117,137,142,147,153,164,170,181,199],{"__ignoreMap":51},[55,56,59],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"sJ8bj","# WRONG — default expire_on_commit=True causes MissingGreenlet after commit\n",[55,64,66,70,74,77],{"class":57,"line":65},2,[55,67,69],{"class":68},"szBVR","from",[55,71,73],{"class":72},"sVt8B"," sqlalchemy.ext.asyncio ",[55,75,76],{"class":68},"import",[55,78,79],{"class":72}," create_async_engine, async_sessionmaker, AsyncSession\n",[55,81,83],{"class":57,"line":82},3,[55,84,86],{"emptyLinePlaceholder":85},true,"\n",[55,88,90,93,96,99,103],{"class":57,"line":89},4,[55,91,92],{"class":72},"engine ",[55,94,95],{"class":68},"=",[55,97,98],{"class":72}," create_async_engine(",[55,100,102],{"class":101},"sZZnC","\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\"",[55,104,105],{"class":72},")\n",[55,107,109],{"class":57,"line":108},5,[55,110,86],{"emptyLinePlaceholder":85},[55,112,114],{"class":57,"line":113},6,[55,115,116],{"class":61},"# Omitting expire_on_commit means it defaults to True — dangerous in async FastAPI\n",[55,118,120,123,125,128,132,134],{"class":57,"line":119},7,[55,121,122],{"class":72},"bad_factory ",[55,124,95],{"class":68},[55,126,127],{"class":72}," async_sessionmaker(engine, ",[55,129,131],{"class":130},"s4XuR","class_",[55,133,95],{"class":68},[55,135,136],{"class":72},"AsyncSession)\n",[55,138,140],{"class":57,"line":139},8,[55,141,86],{"emptyLinePlaceholder":85},[55,143,145],{"class":57,"line":144},9,[55,146,86],{"emptyLinePlaceholder":85},[55,148,150],{"class":57,"line":149},10,[55,151,152],{"class":61},"# CORRECT — expire_on_commit=False keeps in-memory attribute values after commit\n",[55,154,156,159,161],{"class":57,"line":155},11,[55,157,158],{"class":72},"good_factory ",[55,160,95],{"class":68},[55,162,163],{"class":72}," async_sessionmaker(\n",[55,165,167],{"class":57,"line":166},12,[55,168,169],{"class":72},"    engine,\n",[55,171,173,176,178],{"class":57,"line":172},13,[55,174,175],{"class":130},"    class_",[55,177,95],{"class":68},[55,179,180],{"class":72},"AsyncSession,\n",[55,182,184,187,189,193,196],{"class":57,"line":183},14,[55,185,186],{"class":130},"    expire_on_commit",[55,188,95],{"class":68},[55,190,192],{"class":191},"sj4cs","False",[55,194,195],{"class":72},",  ",[55,197,198],{"class":61},"# This one setting prevents the post-commit MissingGreenlet\n",[55,200,202],{"class":57,"line":201},15,[55,203,105],{"class":72},[14,205,206,207,210,211,215,216,219,220,222,223,225],{},"In a FastAPI route that returns an ORM model (or one that Pydantic validates via ",[18,208,209],{},"model_validate","), the response serializer reads instance attributes ",[212,213,214],"em",{},"after"," the dependency's commit fires. With the default ",[18,217,218],{},"expire_on_commit=True",", those attributes are expired, triggering a lazy SELECT that raises ",[18,221,32],{},". With ",[18,224,20],{},", the pre-commit in-memory values remain available for as long as the Python object is referenced.",[41,227,229],{"id":228},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[231,232,234],"h3",{"id":233},"why-sqlalchemy-expires-attributes-by-default","Why SQLAlchemy expires attributes by default",[14,236,237,238,241,242,245],{},"SQLAlchemy's identity map tracks every attribute on every ORM instance. After a ",[18,239,240],{},"commit()",", the authoritative state of the row is in the database, not in Python memory. A DB trigger might have updated a timestamp. A ",[18,243,244],{},"DEFAULT"," expression might have set a column the Python code never touched. An advisory lock release might have allowed a concurrent UPDATE to change a value. To prevent stale reads on the next access, SQLAlchemy marks all attributes as expired after commit. The next attribute read then fetches a fresh value from the database.",[14,247,248,249,252,253,256,257,260,261,264],{},"In a synchronous session, expiry is harmless: ",[18,250,251],{},"user.email"," triggers a ",[18,254,255],{},"SELECT users WHERE id=? LIMIT 1",", the value is returned, and execution continues. In an ",[18,258,259],{},"AsyncSession",", however, the lazy SELECT cannot run synchronously. The greenlet frame that managed the async DBAPI bridge was torn down when the session's ",[18,262,263],{},"async with session.begin()"," context exited. Attempting a lazy SELECT at that point raises:",[46,266,271],{"className":267,"code":269,"language":270},[268],"language-text","sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called;\ncan't call await_only() here. Was IO attempted in an unexpected place?\n","text",[18,272,269],{"__ignoreMap":51},[231,274,276],{"id":275},"the-exact-fastapi-commit-timeline","The exact FastAPI commit timeline",[14,278,279],{},"Understanding precisely when commit fires relative to Pydantic serialization clarifies why this error appears in places that look unrelated to the database:",[281,282,283,290,293,306,313,323],"ol",{},[284,285,286,287,39],"li",{},"Route handler body completes and returns the ORM object, e.g. ",[18,288,289],{},"return user",[284,291,292],{},"FastAPI prepares to send the HTTP response. Before serializing, it must exit the dependency context managers in reverse order.",[284,294,295,296,299,300,302,303,305],{},"The ",[18,297,298],{},"get_db_session"," dependency's ",[18,301,263],{}," block exits — ",[18,304,28],{}," fires.",[284,307,308,309,312],{},"SQLAlchemy marks every attribute on every tracked instance (including ",[18,310,311],{},"user",") as expired.",[284,314,315,316,319,320,39],{},"FastAPI calls the ",[18,317,318],{},"response_model"," validator, e.g. Pydantic's ",[18,321,322],{},"UserRead.model_validate(user)",[284,324,325,326,329,330,39],{},"Pydantic reads ",[18,327,328],{},"user.id"," → expired → lazy SELECT attempt → ",[18,331,32],{},[14,333,334],{},"Step 6 is where the stack trace appears — inside Pydantic's validator, pointing at seemingly innocent attribute access. Developers are frequently confused because the traceback shows no obvious database code; the error appears to originate in the serialization layer.",[231,336,338],{"id":337},"expire_on_commitfalse-eliminates-step-4","expire_on_commit=False eliminates step 4",[14,340,341,342,344,345,347,348,350,351,350,353,356],{},"With ",[18,343,20],{}," configured in ",[18,346,24],{},", step 4 is skipped entirely. Attributes retain their pre-commit Python-side values. When Pydantic reads ",[18,349,328],{},", ",[18,352,251],{},[18,354,355],{},"user.created_at"," in step 6, it reads from the Python dictionary that has been in memory since the instance was loaded — no database I\u002FO, no greenlet bridge required.",[46,358,360],{"className":48,"code":359,"language":50,"meta":51,"style":51},"from contextlib import asynccontextmanager\nfrom typing import AsyncGenerator\n\nfrom fastapi import Depends, FastAPI\nfrom sqlalchemy.ext.asyncio import (\n    AsyncSession,\n    async_sessionmaker,\n    create_async_engine,\n)\n\nDATABASE_URL = \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost:5432\u002Fappdb\"\n\nengine = create_async_engine(\n    DATABASE_URL,\n    pool_size=10,\n    max_overflow=5,\n    pool_pre_ping=True,\n    pool_recycle=1800,\n)\n\nasync_session_factory = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=False,  # retain attribute values after commit\n)\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:\n    yield\n    await engine.dispose()\n\n\napp = FastAPI(lifespan=lifespan)\n\n\nasync def get_db_session() -> AsyncGenerator[AsyncSession, None]:\n    async with async_session_factory() as session:\n        async with session.begin():\n            yield session  # commit fires when this context exits normally\n",[18,361,362,374,386,390,402,413,418,423,428,432,436,447,451,460,468,480,493,506,519,524,529,539,544,553,567,572,577,582,589,614,620,629,634,639,658,663,668,685,703,714],{"__ignoreMap":51},[55,363,364,366,369,371],{"class":57,"line":58},[55,365,69],{"class":68},[55,367,368],{"class":72}," contextlib ",[55,370,76],{"class":68},[55,372,373],{"class":72}," asynccontextmanager\n",[55,375,376,378,381,383],{"class":57,"line":65},[55,377,69],{"class":68},[55,379,380],{"class":72}," typing ",[55,382,76],{"class":68},[55,384,385],{"class":72}," AsyncGenerator\n",[55,387,388],{"class":57,"line":82},[55,389,86],{"emptyLinePlaceholder":85},[55,391,392,394,397,399],{"class":57,"line":89},[55,393,69],{"class":68},[55,395,396],{"class":72}," fastapi ",[55,398,76],{"class":68},[55,400,401],{"class":72}," Depends, FastAPI\n",[55,403,404,406,408,410],{"class":57,"line":108},[55,405,69],{"class":68},[55,407,73],{"class":72},[55,409,76],{"class":68},[55,411,412],{"class":72}," (\n",[55,414,415],{"class":57,"line":113},[55,416,417],{"class":72},"    AsyncSession,\n",[55,419,420],{"class":57,"line":119},[55,421,422],{"class":72},"    async_sessionmaker,\n",[55,424,425],{"class":57,"line":139},[55,426,427],{"class":72},"    create_async_engine,\n",[55,429,430],{"class":57,"line":144},[55,431,105],{"class":72},[55,433,434],{"class":57,"line":149},[55,435,86],{"emptyLinePlaceholder":85},[55,437,438,441,444],{"class":57,"line":155},[55,439,440],{"class":191},"DATABASE_URL",[55,442,443],{"class":68}," =",[55,445,446],{"class":101}," \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost:5432\u002Fappdb\"\n",[55,448,449],{"class":57,"line":166},[55,450,86],{"emptyLinePlaceholder":85},[55,452,453,455,457],{"class":57,"line":172},[55,454,92],{"class":72},[55,456,95],{"class":68},[55,458,459],{"class":72}," create_async_engine(\n",[55,461,462,465],{"class":57,"line":183},[55,463,464],{"class":191},"    DATABASE_URL",[55,466,467],{"class":72},",\n",[55,469,470,473,475,478],{"class":57,"line":201},[55,471,472],{"class":130},"    pool_size",[55,474,95],{"class":68},[55,476,477],{"class":191},"10",[55,479,467],{"class":72},[55,481,483,486,488,491],{"class":57,"line":482},16,[55,484,485],{"class":130},"    max_overflow",[55,487,95],{"class":68},[55,489,490],{"class":191},"5",[55,492,467],{"class":72},[55,494,496,499,501,504],{"class":57,"line":495},17,[55,497,498],{"class":130},"    pool_pre_ping",[55,500,95],{"class":68},[55,502,503],{"class":191},"True",[55,505,467],{"class":72},[55,507,509,512,514,517],{"class":57,"line":508},18,[55,510,511],{"class":130},"    pool_recycle",[55,513,95],{"class":68},[55,515,516],{"class":191},"1800",[55,518,467],{"class":72},[55,520,522],{"class":57,"line":521},19,[55,523,105],{"class":72},[55,525,527],{"class":57,"line":526},20,[55,528,86],{"emptyLinePlaceholder":85},[55,530,532,535,537],{"class":57,"line":531},21,[55,533,534],{"class":72},"async_session_factory ",[55,536,95],{"class":68},[55,538,163],{"class":72},[55,540,542],{"class":57,"line":541},22,[55,543,169],{"class":72},[55,545,547,549,551],{"class":57,"line":546},23,[55,548,175],{"class":130},[55,550,95],{"class":68},[55,552,180],{"class":72},[55,554,556,558,560,562,564],{"class":57,"line":555},24,[55,557,186],{"class":130},[55,559,95],{"class":68},[55,561,192],{"class":191},[55,563,195],{"class":72},[55,565,566],{"class":61},"# retain attribute values after commit\n",[55,568,570],{"class":57,"line":569},25,[55,571,105],{"class":72},[55,573,575],{"class":57,"line":574},26,[55,576,86],{"emptyLinePlaceholder":85},[55,578,580],{"class":57,"line":579},27,[55,581,86],{"emptyLinePlaceholder":85},[55,583,585],{"class":57,"line":584},28,[55,586,588],{"class":587},"sScJk","@asynccontextmanager\n",[55,590,592,595,598,601,604,607,609,611],{"class":57,"line":591},29,[55,593,594],{"class":68},"async",[55,596,597],{"class":68}," def",[55,599,600],{"class":587}," lifespan",[55,602,603],{"class":72},"(app: FastAPI) -> AsyncGenerator[",[55,605,606],{"class":191},"None",[55,608,350],{"class":72},[55,610,606],{"class":191},[55,612,613],{"class":72},"]:\n",[55,615,617],{"class":57,"line":616},30,[55,618,619],{"class":68},"    yield\n",[55,621,623,626],{"class":57,"line":622},31,[55,624,625],{"class":68},"    await",[55,627,628],{"class":72}," engine.dispose()\n",[55,630,632],{"class":57,"line":631},32,[55,633,86],{"emptyLinePlaceholder":85},[55,635,637],{"class":57,"line":636},33,[55,638,86],{"emptyLinePlaceholder":85},[55,640,642,645,647,650,653,655],{"class":57,"line":641},34,[55,643,644],{"class":72},"app ",[55,646,95],{"class":68},[55,648,649],{"class":72}," FastAPI(",[55,651,652],{"class":130},"lifespan",[55,654,95],{"class":68},[55,656,657],{"class":72},"lifespan)\n",[55,659,661],{"class":57,"line":660},35,[55,662,86],{"emptyLinePlaceholder":85},[55,664,666],{"class":57,"line":665},36,[55,667,86],{"emptyLinePlaceholder":85},[55,669,671,673,675,678,681,683],{"class":57,"line":670},37,[55,672,594],{"class":68},[55,674,597],{"class":68},[55,676,677],{"class":587}," get_db_session",[55,679,680],{"class":72},"() -> AsyncGenerator[AsyncSession, ",[55,682,606],{"class":191},[55,684,613],{"class":72},[55,686,688,691,694,697,700],{"class":57,"line":687},38,[55,689,690],{"class":68},"    async",[55,692,693],{"class":68}," with",[55,695,696],{"class":72}," async_session_factory() ",[55,698,699],{"class":68},"as",[55,701,702],{"class":72}," session:\n",[55,704,706,709,711],{"class":57,"line":705},39,[55,707,708],{"class":68},"        async",[55,710,693],{"class":68},[55,712,713],{"class":72}," session.begin():\n",[55,715,717,720,723],{"class":57,"line":716},40,[55,718,719],{"class":68},"            yield",[55,721,722],{"class":72}," session  ",[55,724,725],{"class":61},"# commit fires when this context exits normally\n",[231,727,729],{"id":728},"full-route-example-showing-safe-post-commit-attribute-access","Full route example showing safe post-commit attribute access",[46,731,733],{"className":48,"code":732,"language":50,"meta":51,"style":51},"from fastapi import APIRouter, Depends\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom myapp.models import User\nfrom myapp.schemas import UserCreate, UserRead\n\nrouter = APIRouter()\n\n\n@router.post(\"\u002Fusers\u002F\", response_model=UserRead, status_code=201)\nasync def create_user(\n    payload: UserCreate,\n    session: AsyncSession = Depends(get_db_session),\n) -> User:\n    user = User(email=payload.email, display_name=payload.display_name)\n    session.add(user)\n    await session.flush()  # assigns user.id from the DB sequence, within the txn\n    # The dependency's context manager commits here when this function returns.\n    # expire_on_commit=False means user.id and user.email remain readable.\n    return user\n    # Pydantic reads user.id, user.email, user.created_at after commit — all safe.\n",[18,734,735,746,757,761,773,785,789,799,803,807,837,849,854,864,869,895,900,910,915,920,928],{"__ignoreMap":51},[55,736,737,739,741,743],{"class":57,"line":58},[55,738,69],{"class":68},[55,740,396],{"class":72},[55,742,76],{"class":68},[55,744,745],{"class":72}," APIRouter, Depends\n",[55,747,748,750,752,754],{"class":57,"line":65},[55,749,69],{"class":68},[55,751,73],{"class":72},[55,753,76],{"class":68},[55,755,756],{"class":72}," AsyncSession\n",[55,758,759],{"class":57,"line":82},[55,760,86],{"emptyLinePlaceholder":85},[55,762,763,765,768,770],{"class":57,"line":89},[55,764,69],{"class":68},[55,766,767],{"class":72}," myapp.models ",[55,769,76],{"class":68},[55,771,772],{"class":72}," User\n",[55,774,775,777,780,782],{"class":57,"line":108},[55,776,69],{"class":68},[55,778,779],{"class":72}," myapp.schemas ",[55,781,76],{"class":68},[55,783,784],{"class":72}," UserCreate, UserRead\n",[55,786,787],{"class":57,"line":113},[55,788,86],{"emptyLinePlaceholder":85},[55,790,791,794,796],{"class":57,"line":119},[55,792,793],{"class":72},"router ",[55,795,95],{"class":68},[55,797,798],{"class":72}," APIRouter()\n",[55,800,801],{"class":57,"line":139},[55,802,86],{"emptyLinePlaceholder":85},[55,804,805],{"class":57,"line":144},[55,806,86],{"emptyLinePlaceholder":85},[55,808,809,812,815,818,820,822,824,827,830,832,835],{"class":57,"line":149},[55,810,811],{"class":587},"@router.post",[55,813,814],{"class":72},"(",[55,816,817],{"class":101},"\"\u002Fusers\u002F\"",[55,819,350],{"class":72},[55,821,318],{"class":130},[55,823,95],{"class":68},[55,825,826],{"class":72},"UserRead, ",[55,828,829],{"class":130},"status_code",[55,831,95],{"class":68},[55,833,834],{"class":191},"201",[55,836,105],{"class":72},[55,838,839,841,843,846],{"class":57,"line":155},[55,840,594],{"class":68},[55,842,597],{"class":68},[55,844,845],{"class":587}," create_user",[55,847,848],{"class":72},"(\n",[55,850,851],{"class":57,"line":166},[55,852,853],{"class":72},"    payload: UserCreate,\n",[55,855,856,859,861],{"class":57,"line":172},[55,857,858],{"class":72},"    session: AsyncSession ",[55,860,95],{"class":68},[55,862,863],{"class":72}," Depends(get_db_session),\n",[55,865,866],{"class":57,"line":183},[55,867,868],{"class":72},") -> User:\n",[55,870,871,874,876,879,882,884,887,890,892],{"class":57,"line":201},[55,872,873],{"class":72},"    user ",[55,875,95],{"class":68},[55,877,878],{"class":72}," User(",[55,880,881],{"class":130},"email",[55,883,95],{"class":68},[55,885,886],{"class":72},"payload.email, ",[55,888,889],{"class":130},"display_name",[55,891,95],{"class":68},[55,893,894],{"class":72},"payload.display_name)\n",[55,896,897],{"class":57,"line":482},[55,898,899],{"class":72},"    session.add(user)\n",[55,901,902,904,907],{"class":57,"line":495},[55,903,625],{"class":68},[55,905,906],{"class":72}," session.flush()  ",[55,908,909],{"class":61},"# assigns user.id from the DB sequence, within the txn\n",[55,911,912],{"class":57,"line":508},[55,913,914],{"class":61},"    # The dependency's context manager commits here when this function returns.\n",[55,916,917],{"class":57,"line":521},[55,918,919],{"class":61},"    # expire_on_commit=False means user.id and user.email remain readable.\n",[55,921,922,925],{"class":57,"line":526},[55,923,924],{"class":68},"    return",[55,926,927],{"class":72}," user\n",[55,929,930],{"class":57,"line":531},[55,931,932],{"class":61},"    # Pydantic reads user.id, user.email, user.created_at after commit — all safe.\n",[14,934,935,936,939,940,942,943,945],{},"Without ",[18,937,938],{},"await session.flush()"," before returning, ",[18,941,328],{}," would be ",[18,944,606],{}," (the sequence value has not been assigned yet). Flush assigns the PK within the current transaction, so it is available when the function returns and Pydantic serializes the response.",[41,947,949],{"id":948},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[951,952,953,969],"table",{},[954,955,956],"thead",{},[957,958,959,963,966],"tr",{},[960,961,962],"th",{},"Exact error \u002F symptom",[960,964,965],{},"Root cause",[960,967,968],{},"Fix",[970,971,972,991,1014,1034,1055,1081,1101,1123],"tbody",{},[957,973,974,981,986],{},[975,976,977,980],"td",{},[18,978,979],{},"MissingGreenlet: greenlet_spawn has not been called"," raised during Pydantic serialization",[975,982,983,985],{},[18,984,218],{}," expired attributes after commit; lazy SELECT attempted with no async context active",[975,987,988],{},[18,989,990],{},"async_sessionmaker(..., expire_on_commit=False)",[957,992,993,1002,1005],{},[975,994,995,997,998,1001],{},[18,996,32],{}," on ",[18,999,1000],{},"obj.relationship_name"," after commit",[975,1003,1004],{},"Relationship collection also expired by default; lazy load attempted outside async context",[975,1006,1007,1009,1010,1013],{},[18,1008,20],{}," + eager-load with ",[18,1011,1012],{},"selectinload"," on the original query",[957,1015,1016,1022,1025],{},[975,1017,1018,1021],{},[18,1019,1020],{},"DetachedInstanceError: Instance \u003CUser> is not bound to a Session"," after session closes",[975,1023,1024],{},"Expired instance accessed after the session context exited entirely",[975,1026,1027,1029,1030,1033],{},[18,1028,20],{},"; or ",[18,1031,1032],{},"await session.refresh(obj)"," before session closes",[957,1035,1036,1043,1049],{},[975,1037,1038,1039,1042],{},"Stale ",[18,1040,1041],{},"updated_at"," timestamp returned in JSON response",[975,1044,1045,1046,1048],{},"DB trigger updated the column; ",[18,1047,20],{}," returns the pre-commit Python value, not the trigger-set value",[975,1050,1051,1054],{},[18,1052,1053],{},"await session.refresh(obj, attribute_names=[\"updated_at\"])"," before returning from route",[957,1056,1057,1066,1072],{},[975,1058,1059,1061,1062,1065],{},[18,1060,606],{}," returned for auto-generated PK (",[18,1063,1064],{},"id",") in response",[975,1067,1068,1071],{},[18,1069,1070],{},"flush()"," not called before commit; DB sequence value was never populated in Python",[975,1073,1074,1076,1077,1080],{},[18,1075,938],{}," immediately after ",[18,1078,1079],{},"session.add(obj)",", before returning the object",[957,1082,1083,1089,1097],{},[975,1084,1085,1088],{},[18,1086,1087],{},"ValidationError"," from Pydantic on a field that is visibly populated in the constructor",[975,1090,1091,1092,1094,1095],{},"Same as ",[18,1093,32],{}," but Pydantic catches the exception internally and re-raises as ",[18,1096,1087],{},[975,1098,1099],{},[18,1100,20],{},[957,1102,1103,1111,1117],{},[975,1104,1105,997,1108],{},[18,1106,1107],{},"StaleDataError",[18,1109,1110],{},"session.commit()",[975,1112,1113,1114],{},"Optimistic locking version counter mismatch between Python-side value and DB-side value — unrelated to ",[18,1115,1116],{},"expire_on_commit",[975,1118,1119,1120,1122],{},"Reload the instance with ",[18,1121,1032],{}," and retry the update",[957,1124,1125,1128,1133],{},[975,1126,1127],{},"Background task reads stale data",[975,1129,1130,1131],{},"Request session committed; background task reuses same session which now has expired state with ",[18,1132,218],{},[975,1134,1135,1136,1139],{},"Background tasks must open their own ",[18,1137,1138],{},"async_session_factory()"," context — never share the request session",[41,1141,1143],{"id":1142},"advanced-expire_on_commitfalse-optimization","Advanced expire_on_commit=False Optimization",[231,1145,1147],{"id":1146},"refreshing-only-db-generated-columns","Refreshing only DB-generated columns",[14,1149,1150,1152,1153,1156,1157,1160],{},[18,1151,20],{}," keeps the pre-commit Python values. If the database populates a column that your Python code did not set — a ",[18,1154,1155],{},"DEFAULT NOW()"," timestamp, a trigger-computed field, a ",[18,1158,1159],{},"GENERATED ALWAYS AS"," column — that column's in-memory value will be stale after commit until you explicitly refresh it.",[14,1162,1163],{},"The pattern is to refresh only the specific column(s) you need, within the transaction, before the dependency commits:",[46,1165,1167],{"className":48,"code":1166,"language":50,"meta":51,"style":51},"from sqlalchemy.ext.asyncio import AsyncSession\nfrom myapp.models import Invoice\n\n\nasync def create_invoice(session: AsyncSession, data: dict) -> Invoice:\n    invoice = Invoice(**data)\n    session.add(invoice)\n    await session.flush()  # invoice.id assigned by sequence\n\n    # The DB has a trigger that populates invoice.invoice_number.\n    # Refresh only that column — still within the transaction.\n    await session.refresh(invoice, attribute_names=[\"invoice_number\"])\n\n    # Dependency commits here on function return.\n    # expire_on_commit=False keeps invoice.id, invoice.invoice_number, all other\n    # attributes readable when Pydantic serializes the response.\n    return invoice\n",[18,1168,1169,1179,1190,1194,1198,1216,1232,1237,1246,1250,1255,1260,1281,1285,1290,1295,1300],{"__ignoreMap":51},[55,1170,1171,1173,1175,1177],{"class":57,"line":58},[55,1172,69],{"class":68},[55,1174,73],{"class":72},[55,1176,76],{"class":68},[55,1178,756],{"class":72},[55,1180,1181,1183,1185,1187],{"class":57,"line":65},[55,1182,69],{"class":68},[55,1184,767],{"class":72},[55,1186,76],{"class":68},[55,1188,1189],{"class":72}," Invoice\n",[55,1191,1192],{"class":57,"line":82},[55,1193,86],{"emptyLinePlaceholder":85},[55,1195,1196],{"class":57,"line":89},[55,1197,86],{"emptyLinePlaceholder":85},[55,1199,1200,1202,1204,1207,1210,1213],{"class":57,"line":108},[55,1201,594],{"class":68},[55,1203,597],{"class":68},[55,1205,1206],{"class":587}," create_invoice",[55,1208,1209],{"class":72},"(session: AsyncSession, data: ",[55,1211,1212],{"class":191},"dict",[55,1214,1215],{"class":72},") -> Invoice:\n",[55,1217,1218,1221,1223,1226,1229],{"class":57,"line":113},[55,1219,1220],{"class":72},"    invoice ",[55,1222,95],{"class":68},[55,1224,1225],{"class":72}," Invoice(",[55,1227,1228],{"class":68},"**",[55,1230,1231],{"class":72},"data)\n",[55,1233,1234],{"class":57,"line":119},[55,1235,1236],{"class":72},"    session.add(invoice)\n",[55,1238,1239,1241,1243],{"class":57,"line":139},[55,1240,625],{"class":68},[55,1242,906],{"class":72},[55,1244,1245],{"class":61},"# invoice.id assigned by sequence\n",[55,1247,1248],{"class":57,"line":144},[55,1249,86],{"emptyLinePlaceholder":85},[55,1251,1252],{"class":57,"line":149},[55,1253,1254],{"class":61},"    # The DB has a trigger that populates invoice.invoice_number.\n",[55,1256,1257],{"class":57,"line":155},[55,1258,1259],{"class":61},"    # Refresh only that column — still within the transaction.\n",[55,1261,1262,1264,1267,1270,1272,1275,1278],{"class":57,"line":166},[55,1263,625],{"class":68},[55,1265,1266],{"class":72}," session.refresh(invoice, ",[55,1268,1269],{"class":130},"attribute_names",[55,1271,95],{"class":68},[55,1273,1274],{"class":72},"[",[55,1276,1277],{"class":101},"\"invoice_number\"",[55,1279,1280],{"class":72},"])\n",[55,1282,1283],{"class":57,"line":172},[55,1284,86],{"emptyLinePlaceholder":85},[55,1286,1287],{"class":57,"line":183},[55,1288,1289],{"class":61},"    # Dependency commits here on function return.\n",[55,1291,1292],{"class":57,"line":201},[55,1293,1294],{"class":61},"    # expire_on_commit=False keeps invoice.id, invoice.invoice_number, all other\n",[55,1296,1297],{"class":57,"line":482},[55,1298,1299],{"class":61},"    # attributes readable when Pydantic serializes the response.\n",[55,1301,1302,1304],{"class":57,"line":495},[55,1303,924],{"class":68},[55,1305,1306],{"class":72}," invoice\n",[14,1308,1309,1312,1313,1316],{},[18,1310,1311],{},"session.refresh(obj, attribute_names=[...])"," re-selects only the named columns from the database, within the current transaction, and populates them in the Python object. It is more efficient than ",[18,1314,1315],{},"session.refresh(obj)"," with no arguments, which re-fetches all columns.",[231,1318,1320],{"id":1319},"two-session-factories-for-different-use-cases","Two session factories for different use cases",[14,1322,1323,1324,1326],{},"If your application mixes FastAPI request handlers (where ",[18,1325,20],{}," is correct) and background data pipelines (where you want fresh DB reads after every commit), declare two factories:",[46,1328,1330],{"className":48,"code":1329,"language":50,"meta":51,"style":51},"from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine\n\nengine = create_async_engine(\"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\", pool_size=20)\n\n# FastAPI request handlers: safe post-commit attribute access for serialization\nrequest_session_factory = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=False,\n)\n\n# Background pipelines and scheduled jobs: always re-read from DB after commit\npipeline_session_factory = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=True,  # explicit; reload from DB on next access after commit\n)\n",[18,1331,1332,1343,1347,1369,1373,1378,1387,1391,1399,1409,1413,1417,1422,1431,1435,1443,1456],{"__ignoreMap":51},[55,1333,1334,1336,1338,1340],{"class":57,"line":58},[55,1335,69],{"class":68},[55,1337,73],{"class":72},[55,1339,76],{"class":68},[55,1341,1342],{"class":72}," async_sessionmaker, AsyncSession, create_async_engine\n",[55,1344,1345],{"class":57,"line":65},[55,1346,86],{"emptyLinePlaceholder":85},[55,1348,1349,1351,1353,1355,1357,1359,1362,1364,1367],{"class":57,"line":82},[55,1350,92],{"class":72},[55,1352,95],{"class":68},[55,1354,98],{"class":72},[55,1356,102],{"class":101},[55,1358,350],{"class":72},[55,1360,1361],{"class":130},"pool_size",[55,1363,95],{"class":68},[55,1365,1366],{"class":191},"20",[55,1368,105],{"class":72},[55,1370,1371],{"class":57,"line":89},[55,1372,86],{"emptyLinePlaceholder":85},[55,1374,1375],{"class":57,"line":108},[55,1376,1377],{"class":61},"# FastAPI request handlers: safe post-commit attribute access for serialization\n",[55,1379,1380,1383,1385],{"class":57,"line":113},[55,1381,1382],{"class":72},"request_session_factory ",[55,1384,95],{"class":68},[55,1386,163],{"class":72},[55,1388,1389],{"class":57,"line":119},[55,1390,169],{"class":72},[55,1392,1393,1395,1397],{"class":57,"line":139},[55,1394,175],{"class":130},[55,1396,95],{"class":68},[55,1398,180],{"class":72},[55,1400,1401,1403,1405,1407],{"class":57,"line":144},[55,1402,186],{"class":130},[55,1404,95],{"class":68},[55,1406,192],{"class":191},[55,1408,467],{"class":72},[55,1410,1411],{"class":57,"line":149},[55,1412,105],{"class":72},[55,1414,1415],{"class":57,"line":155},[55,1416,86],{"emptyLinePlaceholder":85},[55,1418,1419],{"class":57,"line":166},[55,1420,1421],{"class":61},"# Background pipelines and scheduled jobs: always re-read from DB after commit\n",[55,1423,1424,1427,1429],{"class":57,"line":172},[55,1425,1426],{"class":72},"pipeline_session_factory ",[55,1428,95],{"class":68},[55,1430,163],{"class":72},[55,1432,1433],{"class":57,"line":183},[55,1434,169],{"class":72},[55,1436,1437,1439,1441],{"class":57,"line":201},[55,1438,175],{"class":130},[55,1440,95],{"class":68},[55,1442,180],{"class":72},[55,1444,1445,1447,1449,1451,1453],{"class":57,"line":482},[55,1446,186],{"class":130},[55,1448,95],{"class":68},[55,1450,503],{"class":191},[55,1452,195],{"class":72},[55,1454,1455],{"class":61},"# explicit; reload from DB on next access after commit\n",[55,1457,1458],{"class":57,"line":495},[55,1459,105],{"class":72},[14,1461,1462],{},"Using two named factories makes the intent explicit in code review and prevents a background pipeline developer from inadvertently relying on stale cached values.",[231,1464,1466],{"id":1465},"testing-with-expire_on_commitfalse","Testing with expire_on_commit=False",[14,1468,1469,1470,1472,1473,1476],{},"When writing pytest tests that use ",[18,1471,20],{}," sessions, test assertions after commit may not catch bugs where a DB constraint changes a value (e.g., a ",[18,1474,1475],{},"CHECK"," rounds a price, a trigger normalises a status string). The Python-side value looks correct because it was never refreshed. Two strategies:",[14,1478,1479,1486],{},[1480,1481,1482,1483,1485],"strong",{},"Strategy 1 — Use ",[18,1484,218],{}," in test sessions."," Tests re-read from the DB, catching constraint-driven changes. Accept that you must eager-load relationships explicitly in test queries.",[14,1488,1489,1492,1493,1495,1496,1498],{},[1480,1490,1491],{},"Strategy 2 — Explicit refresh in assertion blocks."," Keep ",[18,1494,20],{}," in the test session factory (to match production) but add ",[18,1497,1032],{}," in the assertion section of tests that check DB-computed values:",[46,1500,1502],{"className":48,"code":1501,"language":50,"meta":51,"style":51},"import pytest\nfrom sqlalchemy.ext.asyncio import AsyncSession\nfrom myapp.models import Product\n\n\n@pytest.mark.asyncio\nasync def test_price_is_floored_by_constraint(session: AsyncSession) -> None:\n    product = Product(name=\"Widget\", price=0.001)\n    session.add(product)\n    await session.flush()\n\n    # Force a re-read to catch any DB-side CHECK constraint rounding\n    await session.refresh(product)\n    assert product.price >= Decimal(\"0.01\"), \"Price floor constraint not applied\"\n",[18,1503,1504,1511,1521,1532,1536,1540,1545,1562,1592,1597,1604,1608,1613,1620],{"__ignoreMap":51},[55,1505,1506,1508],{"class":57,"line":58},[55,1507,76],{"class":68},[55,1509,1510],{"class":72}," pytest\n",[55,1512,1513,1515,1517,1519],{"class":57,"line":65},[55,1514,69],{"class":68},[55,1516,73],{"class":72},[55,1518,76],{"class":68},[55,1520,756],{"class":72},[55,1522,1523,1525,1527,1529],{"class":57,"line":82},[55,1524,69],{"class":68},[55,1526,767],{"class":72},[55,1528,76],{"class":68},[55,1530,1531],{"class":72}," Product\n",[55,1533,1534],{"class":57,"line":89},[55,1535,86],{"emptyLinePlaceholder":85},[55,1537,1538],{"class":57,"line":108},[55,1539,86],{"emptyLinePlaceholder":85},[55,1541,1542],{"class":57,"line":113},[55,1543,1544],{"class":587},"@pytest.mark.asyncio\n",[55,1546,1547,1549,1551,1554,1557,1559],{"class":57,"line":119},[55,1548,594],{"class":68},[55,1550,597],{"class":68},[55,1552,1553],{"class":587}," test_price_is_floored_by_constraint",[55,1555,1556],{"class":72},"(session: AsyncSession) -> ",[55,1558,606],{"class":191},[55,1560,1561],{"class":72},":\n",[55,1563,1564,1567,1569,1572,1575,1577,1580,1582,1585,1587,1590],{"class":57,"line":139},[55,1565,1566],{"class":72},"    product ",[55,1568,95],{"class":68},[55,1570,1571],{"class":72}," Product(",[55,1573,1574],{"class":130},"name",[55,1576,95],{"class":68},[55,1578,1579],{"class":101},"\"Widget\"",[55,1581,350],{"class":72},[55,1583,1584],{"class":130},"price",[55,1586,95],{"class":68},[55,1588,1589],{"class":191},"0.001",[55,1591,105],{"class":72},[55,1593,1594],{"class":57,"line":144},[55,1595,1596],{"class":72},"    session.add(product)\n",[55,1598,1599,1601],{"class":57,"line":149},[55,1600,625],{"class":68},[55,1602,1603],{"class":72}," session.flush()\n",[55,1605,1606],{"class":57,"line":155},[55,1607,86],{"emptyLinePlaceholder":85},[55,1609,1610],{"class":57,"line":166},[55,1611,1612],{"class":61},"    # Force a re-read to catch any DB-side CHECK constraint rounding\n",[55,1614,1615,1617],{"class":57,"line":172},[55,1616,625],{"class":68},[55,1618,1619],{"class":72}," session.refresh(product)\n",[55,1621,1622,1625,1628,1631,1634,1637,1640],{"class":57,"line":183},[55,1623,1624],{"class":68},"    assert",[55,1626,1627],{"class":72}," product.price ",[55,1629,1630],{"class":68},">=",[55,1632,1633],{"class":72}," Decimal(",[55,1635,1636],{"class":101},"\"0.01\"",[55,1638,1639],{"class":72},"), ",[55,1641,1642],{"class":101},"\"Price floor constraint not applied\"\n",[231,1644,1646],{"id":1645},"per-session-override-without-changing-the-factory","Per-session override without changing the factory",[14,1648,1649,1650,1652],{},"If you need ",[18,1651,20],{}," on a single session without changing the factory default, set it on the instance:",[46,1654,1656],{"className":48,"code":1655,"language":50,"meta":51,"style":51},"async with async_session_factory() as session:\n    session.expire_on_commit = False  # override for this session only\n    async with session.begin():\n        ...\n",[18,1657,1658,1670,1683,1691],{"__ignoreMap":51},[55,1659,1660,1662,1664,1666,1668],{"class":57,"line":58},[55,1661,594],{"class":68},[55,1663,693],{"class":68},[55,1665,696],{"class":72},[55,1667,699],{"class":68},[55,1669,702],{"class":72},[55,1671,1672,1675,1677,1680],{"class":57,"line":65},[55,1673,1674],{"class":72},"    session.expire_on_commit ",[55,1676,95],{"class":68},[55,1678,1679],{"class":191}," False",[55,1681,1682],{"class":61},"  # override for this session only\n",[55,1684,1685,1687,1689],{"class":57,"line":82},[55,1686,690],{"class":68},[55,1688,693],{"class":68},[55,1690,713],{"class":72},[55,1692,1693],{"class":57,"line":89},[55,1694,1695],{"class":191},"        ...\n",[14,1697,1698],{},"Setting it before any objects are loaded ensures all instances in this session are governed by the new setting. This technique is useful in test fixtures that need to override the application's pipeline factory setting for a specific test case.",[41,1700,1702],{"id":1701},"frequently-asked-questions","Frequently Asked Questions",[14,1704,1705,1708,1709,1711,1712,1715,1716,1718,1719,1722],{},[1480,1706,1707],{},"Does expire_on_commit=False serve stale data across requests?","\nNo. Each request receives a fresh ",[18,1710,259],{}," from the ",[18,1713,1714],{},"Depends"," dependency. The ",[18,1717,20],{}," setting only affects instances within the ",[212,1720,1721],{},"same session's"," lifetime. Every new session loads fresh data from the database on its first query.",[14,1724,1725,1728,1729,1732],{},[1480,1726,1727],{},"What happens to relationship collections after commit with expire_on_commit=False?","\nCollections loaded before commit remain populated in memory with their pre-commit content. If a concurrent process deleted a child row, that deletion is not reflected until a new session loads the parent, or until ",[18,1730,1731],{},"await session.refresh(obj, attribute_names=[\"items\"])"," is called explicitly. For request-scoped sessions this is almost never an issue — the session loads data, commits a write, and immediately returns in the same request cycle. No concurrent mutation from another process is expected to interfere within a single request's lifespan.",[14,1734,1735,1738,1739,1741,1742,1744,1745,1747,1748,1750],{},[1480,1736,1737],{},"Should I use expire_on_commit=False in Celery workers or long-running background sessions?","\nGenerally no. Background workers often process items in loops, committing after each item. With ",[18,1740,218],{},", the next loop iteration re-reads the item from the DB, reflecting any changes made by concurrent workers or DB triggers. With ",[18,1743,20],{},", the worker would continue using pre-commit values, potentially acting on stale state. Use ",[18,1746,218],{}," (the default) in background and pipeline sessions, and ",[18,1749,20],{}," only in request-scoped FastAPI dependencies.",[14,1752,1753,1756,1757,1759,1760,1763,1764,1766,1767,1769],{},[1480,1754,1755],{},"Can I set expire_on_commit on a per-commit basis rather than per session?","\nNot directly through the commit call. The ",[18,1758,1116],{}," flag is a session-level property. To prevent expiry for a specific commit only, set ",[18,1761,1762],{},"session.expire_on_commit = False"," before calling ",[18,1765,28],{}," and reset it to ",[18,1768,503],{}," afterwards — though this is fragile in concurrent async code. The cleaner approach is separate session factories as described in the two-factory pattern above.",[41,1771,1773],{"id":1772},"related","Related",[1775,1776,1777,1783,1790],"ul",{},[284,1778,1779,1782],{},[35,1780,1781],{"href":37},"Integrating SQLAlchemy Async with FastAPI and Starlette"," — parent guide covering the full dependency injection lifecycle that this setting is part of.",[284,1784,1785,1789],{},[35,1786,1788],{"href":1787},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Ffixing-greenletspawnerror-in-async-sqlalchemy-workflows\u002F","Fixing GreenletSpawnError in Async SQLAlchemy Workflows"," — all MissingGreenlet root causes, not only the post-commit expiry case covered here.",[284,1791,1792,1796,1797,1799],{},[35,1793,1795],{"href":1794},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools"," — the full engine and ",[18,1798,24],{}," configuration reference these settings live in.",[1801,1802,1803],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":51,"searchDepth":65,"depth":65,"links":1805},[1806,1807,1813,1814,1820,1821],{"id":43,"depth":65,"text":44},{"id":228,"depth":65,"text":229,"children":1808},[1809,1810,1811,1812],{"id":233,"depth":82,"text":234},{"id":275,"depth":82,"text":276},{"id":337,"depth":82,"text":338},{"id":728,"depth":82,"text":729},{"id":948,"depth":65,"text":949},{"id":1142,"depth":65,"text":1143,"children":1815},[1816,1817,1818,1819],{"id":1146,"depth":82,"text":1147},{"id":1319,"depth":82,"text":1320},{"id":1465,"depth":82,"text":1466},{"id":1645,"depth":82,"text":1646},{"id":1701,"depth":65,"text":1702},{"id":1772,"depth":65,"text":1773},"Set expire_on_commit=False in async_sessionmaker and your FastAPI responses will serialize ORM instances correctly after commit — without it, every attribute read after await session.commit() raises MissingGreenlet because SQLAlchemy tries a synchronous lazy reload that has nowhere to run. This page is part of the guide to integrating SQLAlchemy async with FastAPI and Starlette.","md",{"date":1825},"2026-06-18","\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Fusing-expire-on-commit-false-in-fastapi-dependencies",{"title":5,"description":1822},"async-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Fusing-expire-on-commit-false-in-fastapi-dependencies\u002Findex","Rfof1ILvk4Zmg6LE1fHsoWuXpF959Yrz9FOSH0PM7Jk",1781810028983]