[{"data":1,"prerenderedAt":2928},["ShallowReactive",2],{"page-\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Frouting-reads-to-replicas-with-async-engines\u002F":3},{"id":4,"title":5,"body":6,"description":2920,"extension":2921,"meta":2922,"navigation":82,"path":2924,"seo":2925,"stem":2926,"__hash__":2927},"content\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Frouting-reads-to-replicas-with-async-engines\u002Findex.md","Routing Reads to Replicas with Async Engines",{"type":7,"value":8,"toc":2902},"minimark",[9,13,48,53,458,462,467,494,516,631,635,1167,1171,1174,1576,1579,1583,1586,1818,1829,1833,1988,1992,1996,2003,2021,2045,2057,2061,2064,2295,2298,2302,2313,2516,2522,2526,2540,2755,2765,2769,2795,2825,2849,2868,2872,2898],[10,11,5],"h1",{"id":12},"routing-reads-to-replicas-with-async-engines",[14,15,16,17,21,22,25,26,29,30,33,34,37,38,43,44,47],"p",{},"Route read queries to a PostgreSQL replica by creating a second ",[18,19,20],"code",{},"AsyncEngine"," pointing at the replica host, then override ",[18,23,24],{},"get_bind()"," in a ",[18,27,28],{},"Session"," subclass used via ",[18,31,32],{},"AsyncSession(sync_session_class=...)"," to dispatch ",[18,35,36],{},"SELECT"," statements to the replica engine and all writes to the primary. This is the production-safe approach in ",[39,40,42],"a",{"href":41},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002F","Dynamic Schema and Multi-Tenant Routing",", replacing the removed SQLAlchemy 1.4 ",[18,45,46],{},"Session(binds=...)"," API.",[49,50,52],"h2",{"id":51},"quick-answer","Quick Answer",[54,55,60],"pre",{"className":56,"code":57,"language":58,"meta":59,"style":59},"language-python shiki shiki-themes github-light github-dark","# Legacy 1.4 pattern — removed in SQLAlchemy 2.0, do not use\n# session = Session(binds={Order: replica_engine, Invoice: primary_engine})\n\n# SQLAlchemy 2.0 — routing Session subclass\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import Session\nfrom sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker\n\nprimary_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@primary.db.internal\u002Fsaas_db\",\n    pool_size=20,\n    max_overflow=10,\n    pool_pre_ping=True,\n)\n\nreplica_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@replica.db.internal\u002Fsaas_db\",\n    pool_size=30,   # replicas typically absorb more concurrent reads\n    max_overflow=15,\n    pool_pre_ping=True,\n)\n\n\nclass RoutingSession(Session):\n    \"\"\"Sends SELECT statements to the read replica, writes to the primary.\"\"\"\n\n    def get_bind(self, mapper=None, clause=None, **kwargs):\n        if self._flushing or (clause is not None and clause.is_dml):\n            return primary_engine.sync_engine\n        return replica_engine.sync_engine\n\n\nAsyncRoutingSession = async_sessionmaker(\n    class_=AsyncSession,\n    sync_session_class=RoutingSession,\n    expire_on_commit=False,\n)\n","python","",[18,61,62,71,77,84,90,107,120,133,138,150,160,175,188,201,207,212,222,230,246,258,269,274,279,284,302,308,313,346,379,388,397,402,407,418,429,440,453],{"__ignoreMap":59},[63,64,67],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sJ8bj","# Legacy 1.4 pattern — removed in SQLAlchemy 2.0, do not use\n",[63,72,74],{"class":65,"line":73},2,[63,75,76],{"class":69},"# session = Session(binds={Order: replica_engine, Invoice: primary_engine})\n",[63,78,80],{"class":65,"line":79},3,[63,81,83],{"emptyLinePlaceholder":82},true,"\n",[63,85,87],{"class":65,"line":86},4,[63,88,89],{"class":69},"# SQLAlchemy 2.0 — routing Session subclass\n",[63,91,93,97,101,104],{"class":65,"line":92},5,[63,94,96],{"class":95},"szBVR","from",[63,98,100],{"class":99},"sVt8B"," sqlalchemy ",[63,102,103],{"class":95},"import",[63,105,106],{"class":99}," select\n",[63,108,110,112,115,117],{"class":65,"line":109},6,[63,111,96],{"class":95},[63,113,114],{"class":99}," sqlalchemy.orm ",[63,116,103],{"class":95},[63,118,119],{"class":99}," Session\n",[63,121,123,125,128,130],{"class":65,"line":122},7,[63,124,96],{"class":95},[63,126,127],{"class":99}," sqlalchemy.ext.asyncio ",[63,129,103],{"class":95},[63,131,132],{"class":99}," AsyncSession, create_async_engine, async_sessionmaker\n",[63,134,136],{"class":65,"line":135},8,[63,137,83],{"emptyLinePlaceholder":82},[63,139,141,144,147],{"class":65,"line":140},9,[63,142,143],{"class":99},"primary_engine ",[63,145,146],{"class":95},"=",[63,148,149],{"class":99}," create_async_engine(\n",[63,151,153,157],{"class":65,"line":152},10,[63,154,156],{"class":155},"sZZnC","    \"postgresql+asyncpg:\u002F\u002Fuser:pass@primary.db.internal\u002Fsaas_db\"",[63,158,159],{"class":99},",\n",[63,161,163,167,169,173],{"class":65,"line":162},11,[63,164,166],{"class":165},"s4XuR","    pool_size",[63,168,146],{"class":95},[63,170,172],{"class":171},"sj4cs","20",[63,174,159],{"class":99},[63,176,178,181,183,186],{"class":65,"line":177},12,[63,179,180],{"class":165},"    max_overflow",[63,182,146],{"class":95},[63,184,185],{"class":171},"10",[63,187,159],{"class":99},[63,189,191,194,196,199],{"class":65,"line":190},13,[63,192,193],{"class":165},"    pool_pre_ping",[63,195,146],{"class":95},[63,197,198],{"class":171},"True",[63,200,159],{"class":99},[63,202,204],{"class":65,"line":203},14,[63,205,206],{"class":99},")\n",[63,208,210],{"class":65,"line":209},15,[63,211,83],{"emptyLinePlaceholder":82},[63,213,215,218,220],{"class":65,"line":214},16,[63,216,217],{"class":99},"replica_engine ",[63,219,146],{"class":95},[63,221,149],{"class":99},[63,223,225,228],{"class":65,"line":224},17,[63,226,227],{"class":155},"    \"postgresql+asyncpg:\u002F\u002Fuser:pass@replica.db.internal\u002Fsaas_db\"",[63,229,159],{"class":99},[63,231,233,235,237,240,243],{"class":65,"line":232},18,[63,234,166],{"class":165},[63,236,146],{"class":95},[63,238,239],{"class":171},"30",[63,241,242],{"class":99},",   ",[63,244,245],{"class":69},"# replicas typically absorb more concurrent reads\n",[63,247,249,251,253,256],{"class":65,"line":248},19,[63,250,180],{"class":165},[63,252,146],{"class":95},[63,254,255],{"class":171},"15",[63,257,159],{"class":99},[63,259,261,263,265,267],{"class":65,"line":260},20,[63,262,193],{"class":165},[63,264,146],{"class":95},[63,266,198],{"class":171},[63,268,159],{"class":99},[63,270,272],{"class":65,"line":271},21,[63,273,206],{"class":99},[63,275,277],{"class":65,"line":276},22,[63,278,83],{"emptyLinePlaceholder":82},[63,280,282],{"class":65,"line":281},23,[63,283,83],{"emptyLinePlaceholder":82},[63,285,287,290,294,297,299],{"class":65,"line":286},24,[63,288,289],{"class":95},"class",[63,291,293],{"class":292},"sScJk"," RoutingSession",[63,295,296],{"class":99},"(",[63,298,28],{"class":292},[63,300,301],{"class":99},"):\n",[63,303,305],{"class":65,"line":304},25,[63,306,307],{"class":155},"    \"\"\"Sends SELECT statements to the read replica, writes to the primary.\"\"\"\n",[63,309,311],{"class":65,"line":310},26,[63,312,83],{"emptyLinePlaceholder":82},[63,314,316,319,322,325,327,330,333,335,337,340,343],{"class":65,"line":315},27,[63,317,318],{"class":95},"    def",[63,320,321],{"class":292}," get_bind",[63,323,324],{"class":99},"(self, mapper",[63,326,146],{"class":95},[63,328,329],{"class":171},"None",[63,331,332],{"class":99},", clause",[63,334,146],{"class":95},[63,336,329],{"class":171},[63,338,339],{"class":99},", ",[63,341,342],{"class":95},"**",[63,344,345],{"class":99},"kwargs):\n",[63,347,349,352,355,358,361,364,367,370,373,376],{"class":65,"line":348},28,[63,350,351],{"class":95},"        if",[63,353,354],{"class":171}," self",[63,356,357],{"class":99},"._flushing ",[63,359,360],{"class":95},"or",[63,362,363],{"class":99}," (clause ",[63,365,366],{"class":95},"is",[63,368,369],{"class":95}," not",[63,371,372],{"class":171}," None",[63,374,375],{"class":95}," and",[63,377,378],{"class":99}," clause.is_dml):\n",[63,380,382,385],{"class":65,"line":381},29,[63,383,384],{"class":95},"            return",[63,386,387],{"class":99}," primary_engine.sync_engine\n",[63,389,391,394],{"class":65,"line":390},30,[63,392,393],{"class":95},"        return",[63,395,396],{"class":99}," replica_engine.sync_engine\n",[63,398,400],{"class":65,"line":399},31,[63,401,83],{"emptyLinePlaceholder":82},[63,403,405],{"class":65,"line":404},32,[63,406,83],{"emptyLinePlaceholder":82},[63,408,410,413,415],{"class":65,"line":409},33,[63,411,412],{"class":99},"AsyncRoutingSession ",[63,414,146],{"class":95},[63,416,417],{"class":99}," async_sessionmaker(\n",[63,419,421,424,426],{"class":65,"line":420},34,[63,422,423],{"class":165},"    class_",[63,425,146],{"class":95},[63,427,428],{"class":99},"AsyncSession,\n",[63,430,432,435,437],{"class":65,"line":431},35,[63,433,434],{"class":165},"    sync_session_class",[63,436,146],{"class":95},[63,438,439],{"class":99},"RoutingSession,\n",[63,441,443,446,448,451],{"class":65,"line":442},36,[63,444,445],{"class":165},"    expire_on_commit",[63,447,146],{"class":95},[63,449,450],{"class":171},"False",[63,452,159],{"class":99},[63,454,456],{"class":65,"line":455},37,[63,457,206],{"class":99},[49,459,461],{"id":460},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[463,464,466],"h3",{"id":465},"how-get_bind-interacts-with-the-async-layer","How get_bind() Interacts with the Async Layer",[14,468,469,472,473,475,476,479,480,482,483,486,487,489,490,493],{},[18,470,471],{},"AsyncSession"," in SQLAlchemy 2.0 is a thin proxy over a synchronous ",[18,474,28],{},". When ",[18,477,478],{},"AsyncSession.execute()"," is called, it runs the synchronous session machinery in a thread-safe greenlet context managed by SQLAlchemy's async bridge. ",[18,481,24],{}," is called during this synchronous phase, so it has access to the full session state including ",[18,484,485],{},"self._flushing"," (set to ",[18,488,198],{}," during autoflush and explicit ",[18,491,492],{},"session.flush()"," calls).",[14,495,496,497,501,502,505,506,508,509,512,513,515],{},"The returned engine must be a ",[498,499,500],"strong",{},"synchronous"," engine object (",[18,503,504],{},"Engine",", not ",[18,507,20],{},"). SQLAlchemy's async layer wraps it transparently. Use ",[18,510,511],{},"async_engine.sync_engine"," to extract the underlying sync engine from an ",[18,514,20],{},":",[54,517,519],{"className":56,"code":518,"language":58,"meta":59,"style":59},"class RoutingSession(Session):\n    def get_bind(self, mapper=None, clause=None, **kwargs):\n        # self._flushing is True during flush\u002Fcommit write operations\n        if self._flushing:\n            return primary_engine.sync_engine\n        # clause is None for session.get() and relationship loading\n        if clause is None:\n            return replica_engine.sync_engine\n        # DML statements (INSERT, UPDATE, DELETE) go to primary\n        if clause.is_dml:\n            return primary_engine.sync_engine\n        # All SELECT queries go to replica\n        return replica_engine.sync_engine\n",[18,520,521,533,557,562,571,577,582,596,602,607,614,620,625],{"__ignoreMap":59},[63,522,523,525,527,529,531],{"class":65,"line":66},[63,524,289],{"class":95},[63,526,293],{"class":292},[63,528,296],{"class":99},[63,530,28],{"class":292},[63,532,301],{"class":99},[63,534,535,537,539,541,543,545,547,549,551,553,555],{"class":65,"line":73},[63,536,318],{"class":95},[63,538,321],{"class":292},[63,540,324],{"class":99},[63,542,146],{"class":95},[63,544,329],{"class":171},[63,546,332],{"class":99},[63,548,146],{"class":95},[63,550,329],{"class":171},[63,552,339],{"class":99},[63,554,342],{"class":95},[63,556,345],{"class":99},[63,558,559],{"class":65,"line":79},[63,560,561],{"class":69},"        # self._flushing is True during flush\u002Fcommit write operations\n",[63,563,564,566,568],{"class":65,"line":86},[63,565,351],{"class":95},[63,567,354],{"class":171},[63,569,570],{"class":99},"._flushing:\n",[63,572,573,575],{"class":65,"line":92},[63,574,384],{"class":95},[63,576,387],{"class":99},[63,578,579],{"class":65,"line":109},[63,580,581],{"class":69},"        # clause is None for session.get() and relationship loading\n",[63,583,584,586,589,591,593],{"class":65,"line":122},[63,585,351],{"class":95},[63,587,588],{"class":99}," clause ",[63,590,366],{"class":95},[63,592,372],{"class":171},[63,594,595],{"class":99},":\n",[63,597,598,600],{"class":65,"line":135},[63,599,384],{"class":95},[63,601,396],{"class":99},[63,603,604],{"class":65,"line":140},[63,605,606],{"class":69},"        # DML statements (INSERT, UPDATE, DELETE) go to primary\n",[63,608,609,611],{"class":65,"line":152},[63,610,351],{"class":95},[63,612,613],{"class":99}," clause.is_dml:\n",[63,615,616,618],{"class":65,"line":162},[63,617,384],{"class":95},[63,619,387],{"class":99},[63,621,622],{"class":65,"line":177},[63,623,624],{"class":69},"        # All SELECT queries go to replica\n",[63,626,627,629],{"class":65,"line":190},[63,628,393],{"class":95},[63,630,396],{"class":99},[463,632,634],{"id":633},"full-async-session-factory-and-usage","Full Async Session Factory and Usage",[54,636,638],{"className":56,"code":637,"language":58,"meta":59,"style":59},"from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker\nfrom sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    user_id: Mapped[int]\n    total_cents: Mapped[int]\n    fulfilled: Mapped[bool] = mapped_column(default=False)\n\n\nclass RoutingSession(Session):\n    def get_bind(self, mapper=None, clause=None, **kwargs):\n        if self._flushing or (clause is not None and clause.is_dml):\n            return primary_engine.sync_engine\n        return replica_engine.sync_engine\n\n\nAsyncRoutingSession = async_sessionmaker(\n    class_=AsyncSession,\n    sync_session_class=RoutingSession,\n    expire_on_commit=False,\n)\n\n\nasync def get_open_orders(user_id: int) -> list[Order]:\n    \"\"\"Read query — transparently routed to replica.\"\"\"\n    async with AsyncRoutingSession() as session:\n        stmt = select(Order).where(\n            Order.user_id == user_id,\n            Order.fulfilled == False,\n        )\n        result = await session.execute(stmt)\n        return result.scalars().all()\n\n\nasync def fulfill_order(order_id: int) -> None:\n    \"\"\"Write operation — routed to primary via self._flushing.\"\"\"\n    from sqlalchemy import update\n\n    async with AsyncRoutingSession() as session:\n        async with session.begin():\n            stmt = (\n                update(Order)\n                .where(Order.id == order_id)\n                .values(fulfilled=True)\n            )\n            await session.execute(stmt)\n            # session.commit() called by context manager; _flushing=True during flush\n",[18,639,640,651,662,666,670,684,689,693,697,711,721,749,759,768,791,795,799,811,835,857,863,869,873,877,885,893,901,911,915,919,923,942,947,964,974,985,997,1002,1016,1024,1029,1034,1056,1062,1075,1080,1093,1104,1115,1121,1132,1147,1153,1161],{"__ignoreMap":59},[63,641,642,644,646,648],{"class":65,"line":66},[63,643,96],{"class":95},[63,645,127],{"class":99},[63,647,103],{"class":95},[63,649,650],{"class":99}," AsyncSession, async_sessionmaker\n",[63,652,653,655,657,659],{"class":65,"line":73},[63,654,96],{"class":95},[63,656,114],{"class":99},[63,658,103],{"class":95},[63,660,661],{"class":99}," Session, DeclarativeBase, Mapped, mapped_column\n",[63,663,664],{"class":65,"line":79},[63,665,83],{"emptyLinePlaceholder":82},[63,667,668],{"class":65,"line":86},[63,669,83],{"emptyLinePlaceholder":82},[63,671,672,674,677,679,682],{"class":65,"line":92},[63,673,289],{"class":95},[63,675,676],{"class":292}," Base",[63,678,296],{"class":99},[63,680,681],{"class":292},"DeclarativeBase",[63,683,301],{"class":99},[63,685,686],{"class":65,"line":109},[63,687,688],{"class":95},"    pass\n",[63,690,691],{"class":65,"line":122},[63,692,83],{"emptyLinePlaceholder":82},[63,694,695],{"class":65,"line":135},[63,696,83],{"emptyLinePlaceholder":82},[63,698,699,701,704,706,709],{"class":65,"line":140},[63,700,289],{"class":95},[63,702,703],{"class":292}," Order",[63,705,296],{"class":99},[63,707,708],{"class":292},"Base",[63,710,301],{"class":99},[63,712,713,716,718],{"class":65,"line":152},[63,714,715],{"class":99},"    __tablename__ ",[63,717,146],{"class":95},[63,719,720],{"class":155}," \"orders\"\n",[63,722,723,726,729,732,735,737,740,743,745,747],{"class":65,"line":162},[63,724,725],{"class":171},"    id",[63,727,728],{"class":99},": Mapped[",[63,730,731],{"class":171},"int",[63,733,734],{"class":99},"] ",[63,736,146],{"class":95},[63,738,739],{"class":99}," mapped_column(",[63,741,742],{"class":165},"primary_key",[63,744,146],{"class":95},[63,746,198],{"class":171},[63,748,206],{"class":99},[63,750,751,754,756],{"class":65,"line":177},[63,752,753],{"class":99},"    user_id: Mapped[",[63,755,731],{"class":171},[63,757,758],{"class":99},"]\n",[63,760,761,764,766],{"class":65,"line":190},[63,762,763],{"class":99},"    total_cents: Mapped[",[63,765,731],{"class":171},[63,767,758],{"class":99},[63,769,770,773,776,778,780,782,785,787,789],{"class":65,"line":203},[63,771,772],{"class":99},"    fulfilled: Mapped[",[63,774,775],{"class":171},"bool",[63,777,734],{"class":99},[63,779,146],{"class":95},[63,781,739],{"class":99},[63,783,784],{"class":165},"default",[63,786,146],{"class":95},[63,788,450],{"class":171},[63,790,206],{"class":99},[63,792,793],{"class":65,"line":209},[63,794,83],{"emptyLinePlaceholder":82},[63,796,797],{"class":65,"line":214},[63,798,83],{"emptyLinePlaceholder":82},[63,800,801,803,805,807,809],{"class":65,"line":224},[63,802,289],{"class":95},[63,804,293],{"class":292},[63,806,296],{"class":99},[63,808,28],{"class":292},[63,810,301],{"class":99},[63,812,813,815,817,819,821,823,825,827,829,831,833],{"class":65,"line":232},[63,814,318],{"class":95},[63,816,321],{"class":292},[63,818,324],{"class":99},[63,820,146],{"class":95},[63,822,329],{"class":171},[63,824,332],{"class":99},[63,826,146],{"class":95},[63,828,329],{"class":171},[63,830,339],{"class":99},[63,832,342],{"class":95},[63,834,345],{"class":99},[63,836,837,839,841,843,845,847,849,851,853,855],{"class":65,"line":248},[63,838,351],{"class":95},[63,840,354],{"class":171},[63,842,357],{"class":99},[63,844,360],{"class":95},[63,846,363],{"class":99},[63,848,366],{"class":95},[63,850,369],{"class":95},[63,852,372],{"class":171},[63,854,375],{"class":95},[63,856,378],{"class":99},[63,858,859,861],{"class":65,"line":260},[63,860,384],{"class":95},[63,862,387],{"class":99},[63,864,865,867],{"class":65,"line":271},[63,866,393],{"class":95},[63,868,396],{"class":99},[63,870,871],{"class":65,"line":276},[63,872,83],{"emptyLinePlaceholder":82},[63,874,875],{"class":65,"line":281},[63,876,83],{"emptyLinePlaceholder":82},[63,878,879,881,883],{"class":65,"line":286},[63,880,412],{"class":99},[63,882,146],{"class":95},[63,884,417],{"class":99},[63,886,887,889,891],{"class":65,"line":304},[63,888,423],{"class":165},[63,890,146],{"class":95},[63,892,428],{"class":99},[63,894,895,897,899],{"class":65,"line":310},[63,896,434],{"class":165},[63,898,146],{"class":95},[63,900,439],{"class":99},[63,902,903,905,907,909],{"class":65,"line":315},[63,904,445],{"class":165},[63,906,146],{"class":95},[63,908,450],{"class":171},[63,910,159],{"class":99},[63,912,913],{"class":65,"line":348},[63,914,206],{"class":99},[63,916,917],{"class":65,"line":381},[63,918,83],{"emptyLinePlaceholder":82},[63,920,921],{"class":65,"line":390},[63,922,83],{"emptyLinePlaceholder":82},[63,924,925,928,931,934,937,939],{"class":65,"line":399},[63,926,927],{"class":95},"async",[63,929,930],{"class":95}," def",[63,932,933],{"class":292}," get_open_orders",[63,935,936],{"class":99},"(user_id: ",[63,938,731],{"class":171},[63,940,941],{"class":99},") -> list[Order]:\n",[63,943,944],{"class":65,"line":404},[63,945,946],{"class":155},"    \"\"\"Read query — transparently routed to replica.\"\"\"\n",[63,948,949,952,955,958,961],{"class":65,"line":409},[63,950,951],{"class":95},"    async",[63,953,954],{"class":95}," with",[63,956,957],{"class":99}," AsyncRoutingSession() ",[63,959,960],{"class":95},"as",[63,962,963],{"class":99}," session:\n",[63,965,966,969,971],{"class":65,"line":420},[63,967,968],{"class":99},"        stmt ",[63,970,146],{"class":95},[63,972,973],{"class":99}," select(Order).where(\n",[63,975,976,979,982],{"class":65,"line":431},[63,977,978],{"class":99},"            Order.user_id ",[63,980,981],{"class":95},"==",[63,983,984],{"class":99}," user_id,\n",[63,986,987,990,992,995],{"class":65,"line":442},[63,988,989],{"class":99},"            Order.fulfilled ",[63,991,981],{"class":95},[63,993,994],{"class":171}," False",[63,996,159],{"class":99},[63,998,999],{"class":65,"line":455},[63,1000,1001],{"class":99},"        )\n",[63,1003,1005,1008,1010,1013],{"class":65,"line":1004},38,[63,1006,1007],{"class":99},"        result ",[63,1009,146],{"class":95},[63,1011,1012],{"class":95}," await",[63,1014,1015],{"class":99}," session.execute(stmt)\n",[63,1017,1019,1021],{"class":65,"line":1018},39,[63,1020,393],{"class":95},[63,1022,1023],{"class":99}," result.scalars().all()\n",[63,1025,1027],{"class":65,"line":1026},40,[63,1028,83],{"emptyLinePlaceholder":82},[63,1030,1032],{"class":65,"line":1031},41,[63,1033,83],{"emptyLinePlaceholder":82},[63,1035,1037,1039,1041,1044,1047,1049,1052,1054],{"class":65,"line":1036},42,[63,1038,927],{"class":95},[63,1040,930],{"class":95},[63,1042,1043],{"class":292}," fulfill_order",[63,1045,1046],{"class":99},"(order_id: ",[63,1048,731],{"class":171},[63,1050,1051],{"class":99},") -> ",[63,1053,329],{"class":171},[63,1055,595],{"class":99},[63,1057,1059],{"class":65,"line":1058},43,[63,1060,1061],{"class":155},"    \"\"\"Write operation — routed to primary via self._flushing.\"\"\"\n",[63,1063,1065,1068,1070,1072],{"class":65,"line":1064},44,[63,1066,1067],{"class":95},"    from",[63,1069,100],{"class":99},[63,1071,103],{"class":95},[63,1073,1074],{"class":99}," update\n",[63,1076,1078],{"class":65,"line":1077},45,[63,1079,83],{"emptyLinePlaceholder":82},[63,1081,1083,1085,1087,1089,1091],{"class":65,"line":1082},46,[63,1084,951],{"class":95},[63,1086,954],{"class":95},[63,1088,957],{"class":99},[63,1090,960],{"class":95},[63,1092,963],{"class":99},[63,1094,1096,1099,1101],{"class":65,"line":1095},47,[63,1097,1098],{"class":95},"        async",[63,1100,954],{"class":95},[63,1102,1103],{"class":99}," session.begin():\n",[63,1105,1107,1110,1112],{"class":65,"line":1106},48,[63,1108,1109],{"class":99},"            stmt ",[63,1111,146],{"class":95},[63,1113,1114],{"class":99}," (\n",[63,1116,1118],{"class":65,"line":1117},49,[63,1119,1120],{"class":99},"                update(Order)\n",[63,1122,1124,1127,1129],{"class":65,"line":1123},50,[63,1125,1126],{"class":99},"                .where(Order.id ",[63,1128,981],{"class":95},[63,1130,1131],{"class":99}," order_id)\n",[63,1133,1135,1138,1141,1143,1145],{"class":65,"line":1134},51,[63,1136,1137],{"class":99},"                .values(",[63,1139,1140],{"class":165},"fulfilled",[63,1142,146],{"class":95},[63,1144,198],{"class":171},[63,1146,206],{"class":99},[63,1148,1150],{"class":65,"line":1149},52,[63,1151,1152],{"class":99},"            )\n",[63,1154,1156,1159],{"class":65,"line":1155},53,[63,1157,1158],{"class":95},"            await",[63,1160,1015],{"class":99},[63,1162,1164],{"class":65,"line":1163},54,[63,1165,1166],{"class":69},"            # session.commit() called by context manager; _flushing=True during flush\n",[463,1168,1170],{"id":1169},"integrating-replica-routing-with-fastapi","Integrating Replica Routing with FastAPI",[14,1172,1173],{},"In a FastAPI application, expose two dependency functions — one yielding a read-optimized session, one for write sessions — so endpoint functions declare their intent explicitly:",[54,1175,1177],{"className":56,"code":1176,"language":58,"meta":59,"style":59},"from fastapi import Depends\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker\nfrom contextlib import asynccontextmanager\n\nReadSession = async_sessionmaker(\n    class_=AsyncSession,\n    sync_session_class=RoutingSession,  # routes SELECTs to replica\n    expire_on_commit=False,\n)\n\nWriteSession = async_sessionmaker(\n    class_=AsyncSession,\n    sync_session_class=RoutingSession,\n    expire_on_commit=False,\n)\n\n\nasync def get_read_session():\n    async with ReadSession() as session:\n        yield session\n\n\nasync def get_write_session():\n    async with WriteSession() as session:\n        async with session.begin():\n            yield session\n\n\n@app.get(\"\u002Forders\")\nasync def list_orders(\n    user_id: int,\n    session: AsyncSession = Depends(get_read_session),\n):\n    from sqlalchemy import select\n    result = await session.execute(\n        select(Order).where(Order.user_id == user_id)\n    )\n    return result.scalars().all()  # routes to replica automatically\n\n\n@app.post(\"\u002Forders\")\nasync def create_order(\n    payload: dict,\n    session: AsyncSession = Depends(get_write_session),\n):\n    from sqlalchemy import insert\n    await session.execute(insert(Order).values(**payload))\n    # transaction committed by context manager — routes to primary\n",[18,1178,1179,1191,1201,1213,1217,1226,1234,1246,1256,1260,1264,1273,1281,1289,1299,1303,1307,1311,1323,1336,1344,1348,1352,1363,1376,1384,1391,1395,1399,1411,1423,1432,1442,1446,1456,1468,1478,1483,1494,1498,1502,1513,1524,1534,1543,1547,1558,1571],{"__ignoreMap":59},[63,1180,1181,1183,1186,1188],{"class":65,"line":66},[63,1182,96],{"class":95},[63,1184,1185],{"class":99}," fastapi ",[63,1187,103],{"class":95},[63,1189,1190],{"class":99}," Depends\n",[63,1192,1193,1195,1197,1199],{"class":65,"line":73},[63,1194,96],{"class":95},[63,1196,127],{"class":99},[63,1198,103],{"class":95},[63,1200,650],{"class":99},[63,1202,1203,1205,1208,1210],{"class":65,"line":79},[63,1204,96],{"class":95},[63,1206,1207],{"class":99}," contextlib ",[63,1209,103],{"class":95},[63,1211,1212],{"class":99}," asynccontextmanager\n",[63,1214,1215],{"class":65,"line":86},[63,1216,83],{"emptyLinePlaceholder":82},[63,1218,1219,1222,1224],{"class":65,"line":92},[63,1220,1221],{"class":99},"ReadSession ",[63,1223,146],{"class":95},[63,1225,417],{"class":99},[63,1227,1228,1230,1232],{"class":65,"line":109},[63,1229,423],{"class":165},[63,1231,146],{"class":95},[63,1233,428],{"class":99},[63,1235,1236,1238,1240,1243],{"class":65,"line":122},[63,1237,434],{"class":165},[63,1239,146],{"class":95},[63,1241,1242],{"class":99},"RoutingSession,  ",[63,1244,1245],{"class":69},"# routes SELECTs to replica\n",[63,1247,1248,1250,1252,1254],{"class":65,"line":135},[63,1249,445],{"class":165},[63,1251,146],{"class":95},[63,1253,450],{"class":171},[63,1255,159],{"class":99},[63,1257,1258],{"class":65,"line":140},[63,1259,206],{"class":99},[63,1261,1262],{"class":65,"line":152},[63,1263,83],{"emptyLinePlaceholder":82},[63,1265,1266,1269,1271],{"class":65,"line":162},[63,1267,1268],{"class":99},"WriteSession ",[63,1270,146],{"class":95},[63,1272,417],{"class":99},[63,1274,1275,1277,1279],{"class":65,"line":177},[63,1276,423],{"class":165},[63,1278,146],{"class":95},[63,1280,428],{"class":99},[63,1282,1283,1285,1287],{"class":65,"line":190},[63,1284,434],{"class":165},[63,1286,146],{"class":95},[63,1288,439],{"class":99},[63,1290,1291,1293,1295,1297],{"class":65,"line":203},[63,1292,445],{"class":165},[63,1294,146],{"class":95},[63,1296,450],{"class":171},[63,1298,159],{"class":99},[63,1300,1301],{"class":65,"line":209},[63,1302,206],{"class":99},[63,1304,1305],{"class":65,"line":214},[63,1306,83],{"emptyLinePlaceholder":82},[63,1308,1309],{"class":65,"line":224},[63,1310,83],{"emptyLinePlaceholder":82},[63,1312,1313,1315,1317,1320],{"class":65,"line":232},[63,1314,927],{"class":95},[63,1316,930],{"class":95},[63,1318,1319],{"class":292}," get_read_session",[63,1321,1322],{"class":99},"():\n",[63,1324,1325,1327,1329,1332,1334],{"class":65,"line":248},[63,1326,951],{"class":95},[63,1328,954],{"class":95},[63,1330,1331],{"class":99}," ReadSession() ",[63,1333,960],{"class":95},[63,1335,963],{"class":99},[63,1337,1338,1341],{"class":65,"line":260},[63,1339,1340],{"class":95},"        yield",[63,1342,1343],{"class":99}," session\n",[63,1345,1346],{"class":65,"line":271},[63,1347,83],{"emptyLinePlaceholder":82},[63,1349,1350],{"class":65,"line":276},[63,1351,83],{"emptyLinePlaceholder":82},[63,1353,1354,1356,1358,1361],{"class":65,"line":281},[63,1355,927],{"class":95},[63,1357,930],{"class":95},[63,1359,1360],{"class":292}," get_write_session",[63,1362,1322],{"class":99},[63,1364,1365,1367,1369,1372,1374],{"class":65,"line":286},[63,1366,951],{"class":95},[63,1368,954],{"class":95},[63,1370,1371],{"class":99}," WriteSession() ",[63,1373,960],{"class":95},[63,1375,963],{"class":99},[63,1377,1378,1380,1382],{"class":65,"line":304},[63,1379,1098],{"class":95},[63,1381,954],{"class":95},[63,1383,1103],{"class":99},[63,1385,1386,1389],{"class":65,"line":310},[63,1387,1388],{"class":95},"            yield",[63,1390,1343],{"class":99},[63,1392,1393],{"class":65,"line":315},[63,1394,83],{"emptyLinePlaceholder":82},[63,1396,1397],{"class":65,"line":348},[63,1398,83],{"emptyLinePlaceholder":82},[63,1400,1401,1404,1406,1409],{"class":65,"line":381},[63,1402,1403],{"class":292},"@app.get",[63,1405,296],{"class":99},[63,1407,1408],{"class":155},"\"\u002Forders\"",[63,1410,206],{"class":99},[63,1412,1413,1415,1417,1420],{"class":65,"line":390},[63,1414,927],{"class":95},[63,1416,930],{"class":95},[63,1418,1419],{"class":292}," list_orders",[63,1421,1422],{"class":99},"(\n",[63,1424,1425,1428,1430],{"class":65,"line":399},[63,1426,1427],{"class":99},"    user_id: ",[63,1429,731],{"class":171},[63,1431,159],{"class":99},[63,1433,1434,1437,1439],{"class":65,"line":404},[63,1435,1436],{"class":99},"    session: AsyncSession ",[63,1438,146],{"class":95},[63,1440,1441],{"class":99}," Depends(get_read_session),\n",[63,1443,1444],{"class":65,"line":409},[63,1445,301],{"class":99},[63,1447,1448,1450,1452,1454],{"class":65,"line":420},[63,1449,1067],{"class":95},[63,1451,100],{"class":99},[63,1453,103],{"class":95},[63,1455,106],{"class":99},[63,1457,1458,1461,1463,1465],{"class":65,"line":431},[63,1459,1460],{"class":99},"    result ",[63,1462,146],{"class":95},[63,1464,1012],{"class":95},[63,1466,1467],{"class":99}," session.execute(\n",[63,1469,1470,1473,1475],{"class":65,"line":442},[63,1471,1472],{"class":99},"        select(Order).where(Order.user_id ",[63,1474,981],{"class":95},[63,1476,1477],{"class":99}," user_id)\n",[63,1479,1480],{"class":65,"line":455},[63,1481,1482],{"class":99},"    )\n",[63,1484,1485,1488,1491],{"class":65,"line":1004},[63,1486,1487],{"class":95},"    return",[63,1489,1490],{"class":99}," result.scalars().all()  ",[63,1492,1493],{"class":69},"# routes to replica automatically\n",[63,1495,1496],{"class":65,"line":1018},[63,1497,83],{"emptyLinePlaceholder":82},[63,1499,1500],{"class":65,"line":1026},[63,1501,83],{"emptyLinePlaceholder":82},[63,1503,1504,1507,1509,1511],{"class":65,"line":1031},[63,1505,1506],{"class":292},"@app.post",[63,1508,296],{"class":99},[63,1510,1408],{"class":155},[63,1512,206],{"class":99},[63,1514,1515,1517,1519,1522],{"class":65,"line":1036},[63,1516,927],{"class":95},[63,1518,930],{"class":95},[63,1520,1521],{"class":292}," create_order",[63,1523,1422],{"class":99},[63,1525,1526,1529,1532],{"class":65,"line":1058},[63,1527,1528],{"class":99},"    payload: ",[63,1530,1531],{"class":171},"dict",[63,1533,159],{"class":99},[63,1535,1536,1538,1540],{"class":65,"line":1064},[63,1537,1436],{"class":99},[63,1539,146],{"class":95},[63,1541,1542],{"class":99}," Depends(get_write_session),\n",[63,1544,1545],{"class":65,"line":1077},[63,1546,301],{"class":99},[63,1548,1549,1551,1553,1555],{"class":65,"line":1082},[63,1550,1067],{"class":95},[63,1552,100],{"class":99},[63,1554,103],{"class":95},[63,1556,1557],{"class":99}," insert\n",[63,1559,1560,1563,1566,1568],{"class":65,"line":1095},[63,1561,1562],{"class":95},"    await",[63,1564,1565],{"class":99}," session.execute(insert(Order).values(",[63,1567,342],{"class":95},[63,1569,1570],{"class":99},"payload))\n",[63,1572,1573],{"class":65,"line":1106},[63,1574,1575],{"class":69},"    # transaction committed by context manager — routes to primary\n",[14,1577,1578],{},"This separation makes it trivially auditable which endpoints hit the primary and which hit the replica, and avoids accidental primary writes in read-only route handlers.",[463,1580,1582],{"id":1581},"configuring-both-engines-for-production","Configuring Both Engines for Production",[14,1584,1585],{},"The primary and replica engines need independent pool sizing. Reads typically outnumber writes by 5–10× on OLTP workloads, so the replica pool should be larger:",[54,1587,1589],{"className":56,"code":1588,"language":58,"meta":59,"style":59},"from sqlalchemy.ext.asyncio import create_async_engine\n\nprimary_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp:secret@primary.rds.amazonaws.com:5432\u002Fsaas\",\n    pool_size=15,\n    max_overflow=5,\n    pool_timeout=30,\n    pool_recycle=1800,   # recycle before RDS 30-minute idle timeout\n    pool_pre_ping=True,\n    connect_args={\n        \"server_settings\": {\"application_name\": \"saas-primary\"},\n    },\n)\n\nreplica_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp:secret@replica.rds.amazonaws.com:5432\u002Fsaas\",\n    pool_size=25,\n    max_overflow=10,\n    pool_timeout=30,\n    pool_recycle=1800,\n    pool_pre_ping=True,\n    connect_args={\n        \"server_settings\": {\"application_name\": \"saas-replica\"},\n    },\n)\n",[18,1590,1591,1602,1606,1614,1621,1631,1642,1653,1668,1678,1688,1708,1713,1717,1721,1729,1736,1747,1757,1767,1777,1787,1795,1810,1814],{"__ignoreMap":59},[63,1592,1593,1595,1597,1599],{"class":65,"line":66},[63,1594,96],{"class":95},[63,1596,127],{"class":99},[63,1598,103],{"class":95},[63,1600,1601],{"class":99}," create_async_engine\n",[63,1603,1604],{"class":65,"line":73},[63,1605,83],{"emptyLinePlaceholder":82},[63,1607,1608,1610,1612],{"class":65,"line":79},[63,1609,143],{"class":99},[63,1611,146],{"class":95},[63,1613,149],{"class":99},[63,1615,1616,1619],{"class":65,"line":86},[63,1617,1618],{"class":155},"    \"postgresql+asyncpg:\u002F\u002Fapp:secret@primary.rds.amazonaws.com:5432\u002Fsaas\"",[63,1620,159],{"class":99},[63,1622,1623,1625,1627,1629],{"class":65,"line":92},[63,1624,166],{"class":165},[63,1626,146],{"class":95},[63,1628,255],{"class":171},[63,1630,159],{"class":99},[63,1632,1633,1635,1637,1640],{"class":65,"line":109},[63,1634,180],{"class":165},[63,1636,146],{"class":95},[63,1638,1639],{"class":171},"5",[63,1641,159],{"class":99},[63,1643,1644,1647,1649,1651],{"class":65,"line":122},[63,1645,1646],{"class":165},"    pool_timeout",[63,1648,146],{"class":95},[63,1650,239],{"class":171},[63,1652,159],{"class":99},[63,1654,1655,1658,1660,1663,1665],{"class":65,"line":135},[63,1656,1657],{"class":165},"    pool_recycle",[63,1659,146],{"class":95},[63,1661,1662],{"class":171},"1800",[63,1664,242],{"class":99},[63,1666,1667],{"class":69},"# recycle before RDS 30-minute idle timeout\n",[63,1669,1670,1672,1674,1676],{"class":65,"line":140},[63,1671,193],{"class":165},[63,1673,146],{"class":95},[63,1675,198],{"class":171},[63,1677,159],{"class":99},[63,1679,1680,1683,1685],{"class":65,"line":152},[63,1681,1682],{"class":165},"    connect_args",[63,1684,146],{"class":95},[63,1686,1687],{"class":99},"{\n",[63,1689,1690,1693,1696,1699,1702,1705],{"class":65,"line":162},[63,1691,1692],{"class":155},"        \"server_settings\"",[63,1694,1695],{"class":99},": {",[63,1697,1698],{"class":155},"\"application_name\"",[63,1700,1701],{"class":99},": ",[63,1703,1704],{"class":155},"\"saas-primary\"",[63,1706,1707],{"class":99},"},\n",[63,1709,1710],{"class":65,"line":177},[63,1711,1712],{"class":99},"    },\n",[63,1714,1715],{"class":65,"line":190},[63,1716,206],{"class":99},[63,1718,1719],{"class":65,"line":203},[63,1720,83],{"emptyLinePlaceholder":82},[63,1722,1723,1725,1727],{"class":65,"line":209},[63,1724,217],{"class":99},[63,1726,146],{"class":95},[63,1728,149],{"class":99},[63,1730,1731,1734],{"class":65,"line":214},[63,1732,1733],{"class":155},"    \"postgresql+asyncpg:\u002F\u002Fapp:secret@replica.rds.amazonaws.com:5432\u002Fsaas\"",[63,1735,159],{"class":99},[63,1737,1738,1740,1742,1745],{"class":65,"line":224},[63,1739,166],{"class":165},[63,1741,146],{"class":95},[63,1743,1744],{"class":171},"25",[63,1746,159],{"class":99},[63,1748,1749,1751,1753,1755],{"class":65,"line":232},[63,1750,180],{"class":165},[63,1752,146],{"class":95},[63,1754,185],{"class":171},[63,1756,159],{"class":99},[63,1758,1759,1761,1763,1765],{"class":65,"line":248},[63,1760,1646],{"class":165},[63,1762,146],{"class":95},[63,1764,239],{"class":171},[63,1766,159],{"class":99},[63,1768,1769,1771,1773,1775],{"class":65,"line":260},[63,1770,1657],{"class":165},[63,1772,146],{"class":95},[63,1774,1662],{"class":171},[63,1776,159],{"class":99},[63,1778,1779,1781,1783,1785],{"class":65,"line":271},[63,1780,193],{"class":165},[63,1782,146],{"class":95},[63,1784,198],{"class":171},[63,1786,159],{"class":99},[63,1788,1789,1791,1793],{"class":65,"line":276},[63,1790,1682],{"class":165},[63,1792,146],{"class":95},[63,1794,1687],{"class":99},[63,1796,1797,1799,1801,1803,1805,1808],{"class":65,"line":281},[63,1798,1692],{"class":155},[63,1800,1695],{"class":99},[63,1802,1698],{"class":155},[63,1804,1701],{"class":99},[63,1806,1807],{"class":155},"\"saas-replica\"",[63,1809,1707],{"class":99},[63,1811,1812],{"class":65,"line":286},[63,1813,1712],{"class":99},[63,1815,1816],{"class":65,"line":304},[63,1817,206],{"class":99},[14,1819,1820,1821,1824,1825,1828],{},"The ",[18,1822,1823],{},"application_name"," server setting makes it easy to distinguish primary vs replica connections in ",[18,1826,1827],{},"pg_stat_activity"," and cloud database performance dashboards.",[49,1830,1832],{"id":1831},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[1834,1835,1836,1852],"table",{},[1837,1838,1839],"thead",{},[1840,1841,1842,1846,1849],"tr",{},[1843,1844,1845],"th",{},"Error \u002F Warning",[1843,1847,1848],{},"Root Cause",[1843,1850,1851],{},"Production Fix",[1853,1854,1855,1881,1901,1927,1951,1966],"tbody",{},[1840,1856,1857,1863,1871],{},[1858,1859,1860],"td",{},[18,1861,1862],{},"sqlalchemy.exc.InvalidRequestError: Could not locate a bind configured on mapper ... or this Session",[1858,1864,1865,1867,1868,1870],{},[18,1866,24],{}," returned ",[18,1869,329],{}," or raised an exception; no fallback binding exists.",[1858,1872,1873,1874,1876,1877,1880],{},"Ensure ",[18,1875,24],{}," always returns a valid sync engine. Add a catch-all ",[18,1878,1879],{},"return primary_engine.sync_engine"," as the final line.",[1840,1882,1883,1888,1891],{},[1858,1884,1885],{},[18,1886,1887],{},"asyncpg.exceptions.ReadOnlySQLTransactionError: cannot execute INSERT in a read-only transaction",[1858,1889,1890],{},"A write operation was dispatched to the replica engine, which is configured as read-only at the database level.",[1858,1892,1893,1894,1897,1898,1900],{},"Verify ",[18,1895,1896],{},"clause.is_dml"," check is correct. Also check that ",[18,1899,485],{}," is evaluated before the DML check — autoflush during a relationship traversal can trigger writes unexpectedly.",[1840,1902,1903,1909,1912],{},[1858,1904,1905,1908],{},[18,1906,1907],{},"asyncpg.exceptions.ConnectionDoesNotExistError"," after idle period",[1858,1910,1911],{},"The database server closed an idle TCP connection but the pool still held a handle to it.",[1858,1913,1914,1915,1918,1919,1922,1923,1926],{},"Set ",[18,1916,1917],{},"pool_pre_ping=True"," on both engines. Also set ",[18,1920,1921],{},"pool_recycle"," to a value shorter than the server's ",[18,1924,1925],{},"tcp_keepalives_idle"," or cloud provider's idle connection timeout.",[1840,1928,1929,1934,1940],{},[1858,1930,1931],{},[18,1932,1933],{},"sqlalchemy.exc.DetachedInstanceError: Instance \u003COrder> is not bound to a Session",[1858,1935,1936,1939],{},[18,1937,1938],{},"expire_on_commit=True"," (the default) expired attributes after commit; async code accessed them after the session closed.",[1858,1941,1914,1942,1945,1946,1950],{},[18,1943,1944],{},"expire_on_commit=False"," on the session factory. See the guide on ",[39,1947,1949],{"href":1948},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fintegrating-sqlalchemy-async-with-fastapi-and-starlette\u002Fusing-expire-on-commit-false-in-fastapi-dependencies\u002F","expire_on_commit=False in FastAPI dependencies",".",[1840,1952,1953,1956,1959],{},[1858,1954,1955],{},"Reads returning stale data immediately after a write",[1858,1957,1958],{},"Replica lag: the replica has not yet received or applied the WAL record for the write.",[1858,1960,1961,1962,1965],{},"Do not read from the replica immediately after a write in the same request. Use ",[18,1963,1964],{},"session.get(Order, id)"," routed to the primary for the post-write read, or implement a short replica-lag check before switching to replica mode.",[1840,1967,1968,1973,1978],{},[1858,1969,1970],{},[18,1971,1972],{},"MissingGreenlet: greenlet_spawn has not been called",[1858,1974,1975,1977],{},[18,1976,24],{}," called an async function or triggered an async I\u002FO operation inside the synchronous greenlet context.",[1858,1979,1980,1981,1983,1984,1987],{},"Keep ",[18,1982,24],{}," entirely synchronous. Never ",[18,1985,1986],{},"await"," inside it. Resolve tenant-to-engine mapping from a pre-built in-memory dict, not from an async database lookup.",[49,1989,1991],{"id":1990},"advanced-replica-routing-optimization","Advanced Replica Routing Optimization",[463,1993,1995],{"id":1994},"connection-pool-sizing-for-primary-replica-topology","Connection Pool Sizing for Primary + Replica Topology",[14,1997,1998,1999,2002],{},"With a primary-plus-replica deployment, the total connection load on your PostgreSQL instances is ",[18,2000,2001],{},"(primary_pool_size + primary_max_overflow) + (replica_pool_size + replica_max_overflow)"," per application process. For a FastAPI app with four Uvicorn workers:",[2004,2005,2006,2014],"ul",{},[2007,2008,2009,2010,2013],"li",{},"Primary: ",[18,2011,2012],{},"pool_size=10, max_overflow=5"," → up to 60 connections per instance (4 workers × 15)",[2007,2015,2016,2017,2020],{},"Replica: ",[18,2018,2019],{},"pool_size=20, max_overflow=10"," → up to 120 connections per instance (4 workers × 30)",[14,2022,1914,2023,2026,2027,2030,2031,2033,2034,2037,2038,2041,2042,1950],{},[18,2024,2025],{},"max_connections"," on the replica at least to ",[18,2028,2029],{},"120 + 20"," (application headroom) plus connections from other services. For AWS RDS, ",[18,2032,2025],{}," defaults to approximately ",[18,2035,2036],{},"LEAST(DBInstanceClassMemory\u002F9531392, 5000)"," — a ",[18,2039,2040],{},"db.t3.medium"," (2 GB RAM) gets roughly 170 connections, which is tight with the numbers above. Scale up the instance class or reduce per-worker pool sizes if you hit ",[18,2043,2044],{},"FATAL: remaining connection slots are reserved for non-replication superuser connections",[14,2046,1820,2047,2049,2050,2053,2054,2056],{},[18,2048,1921],{}," parameter is critical for cloud-managed databases. AWS RDS drops idle connections after 8 minutes (480 seconds) by default. Set ",[18,2051,2052],{},"pool_recycle=300"," on both engines to recycle before the server-side timeout, preventing ",[18,2055,1907],{}," on the first query after an idle period.",[463,2058,2060],{"id":2059},"opt-out-routing-for-replica-lag-sensitive-operations","Opt-Out Routing for Replica-Lag-Sensitive Operations",[14,2062,2063],{},"Some reads must go to the primary — for example, reading an entity immediately after creating it to return it in the API response, or reading data that gates a financial transaction. Use a custom execution option as a routing hint:",[54,2065,2067],{"className":56,"code":2066,"language":58,"meta":59,"style":59},"from sqlalchemy.orm import Session\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nclass RoutingSession(Session):\n    def get_bind(self, mapper=None, clause=None, **kwargs):\n        # Check per-statement override first\n        use_primary = self.get_execution_options().get(\"use_primary\", False)\n        if use_primary or self._flushing:\n            return primary_engine.sync_engine\n        if clause is not None and clause.is_dml:\n            return primary_engine.sync_engine\n        return replica_engine.sync_engine\n\n\n# Usage: force read from primary for a specific query\nasync def get_order_post_create(session: AsyncSession, order_id: int) -> Order:\n    stmt = select(Order).where(Order.id == order_id)\n    result = await session.execute(\n        stmt,\n        execution_options={\"use_primary\": True},\n    )\n    return result.scalar_one()\n",[18,2068,2069,2079,2090,2094,2098,2110,2134,2139,2160,2173,2179,2195,2201,2207,2211,2215,2220,2237,2251,2261,2266,2284,2288],{"__ignoreMap":59},[63,2070,2071,2073,2075,2077],{"class":65,"line":66},[63,2072,96],{"class":95},[63,2074,114],{"class":99},[63,2076,103],{"class":95},[63,2078,119],{"class":99},[63,2080,2081,2083,2085,2087],{"class":65,"line":73},[63,2082,96],{"class":95},[63,2084,127],{"class":99},[63,2086,103],{"class":95},[63,2088,2089],{"class":99}," AsyncSession\n",[63,2091,2092],{"class":65,"line":79},[63,2093,83],{"emptyLinePlaceholder":82},[63,2095,2096],{"class":65,"line":86},[63,2097,83],{"emptyLinePlaceholder":82},[63,2099,2100,2102,2104,2106,2108],{"class":65,"line":92},[63,2101,289],{"class":95},[63,2103,293],{"class":292},[63,2105,296],{"class":99},[63,2107,28],{"class":292},[63,2109,301],{"class":99},[63,2111,2112,2114,2116,2118,2120,2122,2124,2126,2128,2130,2132],{"class":65,"line":109},[63,2113,318],{"class":95},[63,2115,321],{"class":292},[63,2117,324],{"class":99},[63,2119,146],{"class":95},[63,2121,329],{"class":171},[63,2123,332],{"class":99},[63,2125,146],{"class":95},[63,2127,329],{"class":171},[63,2129,339],{"class":99},[63,2131,342],{"class":95},[63,2133,345],{"class":99},[63,2135,2136],{"class":65,"line":122},[63,2137,2138],{"class":69},"        # Check per-statement override first\n",[63,2140,2141,2144,2146,2148,2151,2154,2156,2158],{"class":65,"line":135},[63,2142,2143],{"class":99},"        use_primary ",[63,2145,146],{"class":95},[63,2147,354],{"class":171},[63,2149,2150],{"class":99},".get_execution_options().get(",[63,2152,2153],{"class":155},"\"use_primary\"",[63,2155,339],{"class":99},[63,2157,450],{"class":171},[63,2159,206],{"class":99},[63,2161,2162,2164,2167,2169,2171],{"class":65,"line":140},[63,2163,351],{"class":95},[63,2165,2166],{"class":99}," use_primary ",[63,2168,360],{"class":95},[63,2170,354],{"class":171},[63,2172,570],{"class":99},[63,2174,2175,2177],{"class":65,"line":152},[63,2176,384],{"class":95},[63,2178,387],{"class":99},[63,2180,2181,2183,2185,2187,2189,2191,2193],{"class":65,"line":162},[63,2182,351],{"class":95},[63,2184,588],{"class":99},[63,2186,366],{"class":95},[63,2188,369],{"class":95},[63,2190,372],{"class":171},[63,2192,375],{"class":95},[63,2194,613],{"class":99},[63,2196,2197,2199],{"class":65,"line":177},[63,2198,384],{"class":95},[63,2200,387],{"class":99},[63,2202,2203,2205],{"class":65,"line":190},[63,2204,393],{"class":95},[63,2206,396],{"class":99},[63,2208,2209],{"class":65,"line":203},[63,2210,83],{"emptyLinePlaceholder":82},[63,2212,2213],{"class":65,"line":209},[63,2214,83],{"emptyLinePlaceholder":82},[63,2216,2217],{"class":65,"line":214},[63,2218,2219],{"class":69},"# Usage: force read from primary for a specific query\n",[63,2221,2222,2224,2226,2229,2232,2234],{"class":65,"line":224},[63,2223,927],{"class":95},[63,2225,930],{"class":95},[63,2227,2228],{"class":292}," get_order_post_create",[63,2230,2231],{"class":99},"(session: AsyncSession, order_id: ",[63,2233,731],{"class":171},[63,2235,2236],{"class":99},") -> Order:\n",[63,2238,2239,2242,2244,2247,2249],{"class":65,"line":232},[63,2240,2241],{"class":99},"    stmt ",[63,2243,146],{"class":95},[63,2245,2246],{"class":99}," select(Order).where(Order.id ",[63,2248,981],{"class":95},[63,2250,1131],{"class":99},[63,2252,2253,2255,2257,2259],{"class":65,"line":248},[63,2254,1460],{"class":99},[63,2256,146],{"class":95},[63,2258,1012],{"class":95},[63,2260,1467],{"class":99},[63,2262,2263],{"class":65,"line":260},[63,2264,2265],{"class":99},"        stmt,\n",[63,2267,2268,2271,2273,2276,2278,2280,2282],{"class":65,"line":271},[63,2269,2270],{"class":165},"        execution_options",[63,2272,146],{"class":95},[63,2274,2275],{"class":99},"{",[63,2277,2153],{"class":155},[63,2279,1701],{"class":99},[63,2281,198],{"class":171},[63,2283,1707],{"class":99},[63,2285,2286],{"class":65,"line":276},[63,2287,1482],{"class":99},[63,2289,2290,2292],{"class":65,"line":281},[63,2291,1487],{"class":95},[63,2293,2294],{"class":99}," result.scalar_one()\n",[14,2296,2297],{},"This keeps routing logic in a single place while allowing per-call overrides without modifying the session factory.",[463,2299,2301],{"id":2300},"monitoring-replica-lag-before-routing","Monitoring Replica Lag Before Routing",[14,2303,2304,2305,2308,2309,2312],{},"For applications where replica lag is measurable and consequential, query ",[18,2306,2307],{},"pg_stat_replication"," on the primary (or ",[18,2310,2311],{},"pg_last_wal_receive_lsn()"," on the replica) to gate replica usage:",[54,2314,2316],{"className":56,"code":2315,"language":58,"meta":59,"style":59},"from sqlalchemy import text\nfrom sqlalchemy.ext.asyncio import AsyncConnection\n\n\nasync def replica_lag_seconds(conn: AsyncConnection) -> float:\n    \"\"\"Run against the replica engine to measure current replication lag.\"\"\"\n    result = await conn.execute(\n        text(\n            \"SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::float\"\n        )\n    )\n    lag = result.scalar_one_or_none()\n    return lag if lag is not None else 0.0\n\n\nasync def get_engine_for_read(max_lag_seconds: float = 5.0):\n    async with replica_engine.connect() as conn:\n        lag = await replica_lag_seconds(conn)\n    if lag > max_lag_seconds:\n        return primary_engine\n    return replica_engine\n",[18,2317,2318,2329,2340,2344,2348,2365,2370,2381,2386,2391,2395,2399,2409,2433,2437,2441,2463,2477,2489,2502,2509],{"__ignoreMap":59},[63,2319,2320,2322,2324,2326],{"class":65,"line":66},[63,2321,96],{"class":95},[63,2323,100],{"class":99},[63,2325,103],{"class":95},[63,2327,2328],{"class":99}," text\n",[63,2330,2331,2333,2335,2337],{"class":65,"line":73},[63,2332,96],{"class":95},[63,2334,127],{"class":99},[63,2336,103],{"class":95},[63,2338,2339],{"class":99}," AsyncConnection\n",[63,2341,2342],{"class":65,"line":79},[63,2343,83],{"emptyLinePlaceholder":82},[63,2345,2346],{"class":65,"line":86},[63,2347,83],{"emptyLinePlaceholder":82},[63,2349,2350,2352,2354,2357,2360,2363],{"class":65,"line":92},[63,2351,927],{"class":95},[63,2353,930],{"class":95},[63,2355,2356],{"class":292}," replica_lag_seconds",[63,2358,2359],{"class":99},"(conn: AsyncConnection) -> ",[63,2361,2362],{"class":171},"float",[63,2364,595],{"class":99},[63,2366,2367],{"class":65,"line":109},[63,2368,2369],{"class":155},"    \"\"\"Run against the replica engine to measure current replication lag.\"\"\"\n",[63,2371,2372,2374,2376,2378],{"class":65,"line":122},[63,2373,1460],{"class":99},[63,2375,146],{"class":95},[63,2377,1012],{"class":95},[63,2379,2380],{"class":99}," conn.execute(\n",[63,2382,2383],{"class":65,"line":135},[63,2384,2385],{"class":99},"        text(\n",[63,2387,2388],{"class":65,"line":140},[63,2389,2390],{"class":155},"            \"SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::float\"\n",[63,2392,2393],{"class":65,"line":152},[63,2394,1001],{"class":99},[63,2396,2397],{"class":65,"line":162},[63,2398,1482],{"class":99},[63,2400,2401,2404,2406],{"class":65,"line":177},[63,2402,2403],{"class":99},"    lag ",[63,2405,146],{"class":95},[63,2407,2408],{"class":99}," result.scalar_one_or_none()\n",[63,2410,2411,2413,2416,2419,2421,2423,2425,2427,2430],{"class":65,"line":190},[63,2412,1487],{"class":95},[63,2414,2415],{"class":99}," lag ",[63,2417,2418],{"class":95},"if",[63,2420,2415],{"class":99},[63,2422,366],{"class":95},[63,2424,369],{"class":95},[63,2426,372],{"class":171},[63,2428,2429],{"class":95}," else",[63,2431,2432],{"class":171}," 0.0\n",[63,2434,2435],{"class":65,"line":203},[63,2436,83],{"emptyLinePlaceholder":82},[63,2438,2439],{"class":65,"line":209},[63,2440,83],{"emptyLinePlaceholder":82},[63,2442,2443,2445,2447,2450,2453,2455,2458,2461],{"class":65,"line":214},[63,2444,927],{"class":95},[63,2446,930],{"class":95},[63,2448,2449],{"class":292}," get_engine_for_read",[63,2451,2452],{"class":99},"(max_lag_seconds: ",[63,2454,2362],{"class":171},[63,2456,2457],{"class":95}," =",[63,2459,2460],{"class":171}," 5.0",[63,2462,301],{"class":99},[63,2464,2465,2467,2469,2472,2474],{"class":65,"line":224},[63,2466,951],{"class":95},[63,2468,954],{"class":95},[63,2470,2471],{"class":99}," replica_engine.connect() ",[63,2473,960],{"class":95},[63,2475,2476],{"class":99}," conn:\n",[63,2478,2479,2482,2484,2486],{"class":65,"line":232},[63,2480,2481],{"class":99},"        lag ",[63,2483,146],{"class":95},[63,2485,1012],{"class":95},[63,2487,2488],{"class":99}," replica_lag_seconds(conn)\n",[63,2490,2491,2494,2496,2499],{"class":65,"line":248},[63,2492,2493],{"class":95},"    if",[63,2495,2415],{"class":99},[63,2497,2498],{"class":95},">",[63,2500,2501],{"class":99}," max_lag_seconds:\n",[63,2503,2504,2506],{"class":65,"line":260},[63,2505,393],{"class":95},[63,2507,2508],{"class":99}," primary_engine\n",[63,2510,2511,2513],{"class":65,"line":271},[63,2512,1487],{"class":95},[63,2514,2515],{"class":99}," replica_engine\n",[14,2517,2518,2519,2521],{},"In practice, this check adds a round-trip on every request. For most SaaS workloads, a simpler approach is to measure lag in a background health-check task every 10 seconds and store the result in a module-level variable that ",[18,2520,24],{}," reads.",[463,2523,2525],{"id":2524},"combining-replica-routing-with-schema_translate_map","Combining Replica Routing with schema_translate_map",[14,2527,2528,2529,2532,2533,2535,2536,2539],{},"Multi-tenant applications often need both: read-from-replica AND schema isolation. Stack the two patterns by reading the custom routing option from ",[18,2530,2531],{},"execution_options"," in ",[18,2534,24],{},", while letting SQLAlchemy's built-in ",[18,2537,2538],{},"schema_translate_map"," handler rewrite table names independently:",[54,2541,2543],{"className":56,"code":2542,"language":58,"meta":59,"style":59},"class MultiTenantRoutingSession(Session):\n    def get_bind(self, mapper=None, clause=None, **kwargs):\n        opts = self.get_execution_options()\n        if opts.get(\"use_primary\") or self._flushing:\n            return primary_engine.sync_engine\n        if clause is not None and clause.is_dml:\n            return primary_engine.sync_engine\n        return replica_engine.sync_engine\n\n\nasync def fetch_tenant_orders_from_replica(\n    session: AsyncSession, tenant_schema: str, user_id: int\n) -> list[Order]:\n    stmt = select(Order).where(Order.user_id == user_id)\n    result = await session.execute(\n        stmt,\n        execution_options={\n            \"schema_translate_map\": {None: tenant_schema},\n            # No use_primary flag → routes to replica\n        },\n    )\n    return result.scalars().all()\n    # Emits against replica: SELECT ... FROM acme.orders WHERE user_id = ?\n",[18,2544,2545,2558,2582,2594,2612,2618,2634,2640,2646,2650,2654,2665,2679,2683,2696,2706,2710,2718,2730,2735,2740,2744,2750],{"__ignoreMap":59},[63,2546,2547,2549,2552,2554,2556],{"class":65,"line":66},[63,2548,289],{"class":95},[63,2550,2551],{"class":292}," MultiTenantRoutingSession",[63,2553,296],{"class":99},[63,2555,28],{"class":292},[63,2557,301],{"class":99},[63,2559,2560,2562,2564,2566,2568,2570,2572,2574,2576,2578,2580],{"class":65,"line":73},[63,2561,318],{"class":95},[63,2563,321],{"class":292},[63,2565,324],{"class":99},[63,2567,146],{"class":95},[63,2569,329],{"class":171},[63,2571,332],{"class":99},[63,2573,146],{"class":95},[63,2575,329],{"class":171},[63,2577,339],{"class":99},[63,2579,342],{"class":95},[63,2581,345],{"class":99},[63,2583,2584,2587,2589,2591],{"class":65,"line":79},[63,2585,2586],{"class":99},"        opts ",[63,2588,146],{"class":95},[63,2590,354],{"class":171},[63,2592,2593],{"class":99},".get_execution_options()\n",[63,2595,2596,2598,2601,2603,2606,2608,2610],{"class":65,"line":86},[63,2597,351],{"class":95},[63,2599,2600],{"class":99}," opts.get(",[63,2602,2153],{"class":155},[63,2604,2605],{"class":99},") ",[63,2607,360],{"class":95},[63,2609,354],{"class":171},[63,2611,570],{"class":99},[63,2613,2614,2616],{"class":65,"line":92},[63,2615,384],{"class":95},[63,2617,387],{"class":99},[63,2619,2620,2622,2624,2626,2628,2630,2632],{"class":65,"line":109},[63,2621,351],{"class":95},[63,2623,588],{"class":99},[63,2625,366],{"class":95},[63,2627,369],{"class":95},[63,2629,372],{"class":171},[63,2631,375],{"class":95},[63,2633,613],{"class":99},[63,2635,2636,2638],{"class":65,"line":122},[63,2637,384],{"class":95},[63,2639,387],{"class":99},[63,2641,2642,2644],{"class":65,"line":135},[63,2643,393],{"class":95},[63,2645,396],{"class":99},[63,2647,2648],{"class":65,"line":140},[63,2649,83],{"emptyLinePlaceholder":82},[63,2651,2652],{"class":65,"line":152},[63,2653,83],{"emptyLinePlaceholder":82},[63,2655,2656,2658,2660,2663],{"class":65,"line":162},[63,2657,927],{"class":95},[63,2659,930],{"class":95},[63,2661,2662],{"class":292}," fetch_tenant_orders_from_replica",[63,2664,1422],{"class":99},[63,2666,2667,2670,2673,2676],{"class":65,"line":177},[63,2668,2669],{"class":99},"    session: AsyncSession, tenant_schema: ",[63,2671,2672],{"class":171},"str",[63,2674,2675],{"class":99},", user_id: ",[63,2677,2678],{"class":171},"int\n",[63,2680,2681],{"class":65,"line":190},[63,2682,941],{"class":99},[63,2684,2685,2687,2689,2692,2694],{"class":65,"line":203},[63,2686,2241],{"class":99},[63,2688,146],{"class":95},[63,2690,2691],{"class":99}," select(Order).where(Order.user_id ",[63,2693,981],{"class":95},[63,2695,1477],{"class":99},[63,2697,2698,2700,2702,2704],{"class":65,"line":209},[63,2699,1460],{"class":99},[63,2701,146],{"class":95},[63,2703,1012],{"class":95},[63,2705,1467],{"class":99},[63,2707,2708],{"class":65,"line":214},[63,2709,2265],{"class":99},[63,2711,2712,2714,2716],{"class":65,"line":224},[63,2713,2270],{"class":165},[63,2715,146],{"class":95},[63,2717,1687],{"class":99},[63,2719,2720,2723,2725,2727],{"class":65,"line":232},[63,2721,2722],{"class":155},"            \"schema_translate_map\"",[63,2724,1695],{"class":99},[63,2726,329],{"class":171},[63,2728,2729],{"class":99},": tenant_schema},\n",[63,2731,2732],{"class":65,"line":248},[63,2733,2734],{"class":69},"            # No use_primary flag → routes to replica\n",[63,2736,2737],{"class":65,"line":260},[63,2738,2739],{"class":99},"        },\n",[63,2741,2742],{"class":65,"line":271},[63,2743,1482],{"class":99},[63,2745,2746,2748],{"class":65,"line":276},[63,2747,1487],{"class":95},[63,2749,1023],{"class":99},[63,2751,2752],{"class":65,"line":281},[63,2753,2754],{"class":69},"    # Emits against replica: SELECT ... FROM acme.orders WHERE user_id = ?\n",[14,2756,2757,2758,2760,2761,1950],{},"For the full ",[18,2759,2538],{}," wiring with FastAPI, see ",[39,2762,2764],{"href":2763},"\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Fswitching-schemas-per-request-with-schema-translate-map\u002F","Switching Schemas per Request with schema_translate_map",[49,2766,2768],{"id":2767},"frequently-asked-questions","Frequently Asked Questions",[14,2770,2771,2777,2778,2780,2781,2783,2784,2787,2788,2791,2792,2794],{},[498,2772,2773,2774,2776],{},"Does ",[18,2775,24],{}," get called for every statement, including relationship loads?","\nYes. Every SQL emission that goes through the ",[18,2779,28],{}," calls ",[18,2782,24],{},". This includes ",[18,2785,2786],{},"selectinload"," secondary queries, autoflush triggers, and ",[18,2789,2790],{},"session.get()"," identity map misses. Ensure ",[18,2793,24],{}," is fast — it should only consult an in-memory dict, not make I\u002FO calls.",[14,2796,2797,2800,2802,2803,2806,2807,2810,2811,2814,2815,2817,2818,2821,2822,1950],{},[498,2798,2799],{},"What happens if the replica is down?",[18,2801,24],{}," returns the replica engine. When the connection attempt fails, ",[18,2804,2805],{},"asyncpg"," raises ",[18,2808,2809],{},"asyncpg.exceptions.CannotConnectNowError"," or ",[18,2812,2813],{},"OSError",". The ",[18,2816,1917],{}," setting will detect dead connections on checkout and attempt to reconnect within the pool. If all replica connections fail, the pool raises ",[18,2819,2820],{},"sqlalchemy.exc.TimeoutError",". To automatically fall back to the primary, wrap the execute call in a try\u002Fexcept and retry with ",[18,2823,2824],{},"execution_options={\"use_primary\": True}",[14,2826,2827,2837,2838,2840,2841,2844,2845,2848],{},[498,2828,2829,2830,2832,2833,2836],{},"Can I use ",[18,2831,471],{}," without ",[18,2834,2835],{},"sync_session_class"," for replica routing?","\nThe ",[18,2839,2835],{}," approach is the canonical SQLAlchemy 2.0 pattern. An alternative is to skip the routing session entirely and explicitly select the engine per operation — ",[18,2842,2843],{},"async with primary_engine.connect() as conn"," for writes and ",[18,2846,2847],{},"async with replica_engine.connect() as conn"," for reads — at the cost of more verbose application code and no automatic routing.",[14,2850,2851,2854,2855,2858,2859,2861,2862,2864,2865,2867],{},[498,2852,2853],{},"How do I handle transactions that mix reads and writes?","\nInside a ",[18,2856,2857],{},"session.begin()"," block, ",[18,2860,485],{}," becomes ",[18,2863,198],{}," when the transaction is flushed. However, reads that occur before the flush within the same transaction will still go to the replica. If you need transactional consistency (read your own writes), set ",[18,2866,2824],{}," on the reads that must see the in-progress write, or execute the entire transaction against the primary engine directly.",[49,2869,2871],{"id":2870},"related","Related",[2004,2873,2874,2879,2884,2891],{},[2007,2875,2876,2878],{},[39,2877,42],{"href":41}," — Parent guide covering schema isolation models, engine registries, and vertical partitioning.",[2007,2880,2881,2883],{},[39,2882,2764],{"href":2763}," — Combine schema routing with replica routing for full multi-tenant isolation.",[2007,2885,2886,2890],{},[39,2887,2889],{"href":2888},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools"," — Pool sizing guidance for primary and replica engines under production load.",[2007,2892,2893,2897],{},[39,2894,2896],{"href":2895},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002F","Handling Connection Leaks and Pool Exhaustion"," — Diagnose and fix pool exhaustion when replica connections are not returned promptly.",[2899,2900,2901],"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 pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}",{"title":59,"searchDepth":73,"depth":73,"links":2903},[2904,2905,2911,2912,2918,2919],{"id":51,"depth":73,"text":52},{"id":460,"depth":73,"text":461,"children":2906},[2907,2908,2909,2910],{"id":465,"depth":79,"text":466},{"id":633,"depth":79,"text":634},{"id":1169,"depth":79,"text":1170},{"id":1581,"depth":79,"text":1582},{"id":1831,"depth":73,"text":1832},{"id":1990,"depth":73,"text":1991,"children":2913},[2914,2915,2916,2917],{"id":1994,"depth":79,"text":1995},{"id":2059,"depth":79,"text":2060},{"id":2300,"depth":79,"text":2301},{"id":2524,"depth":79,"text":2525},{"id":2767,"depth":73,"text":2768},{"id":2870,"depth":73,"text":2871},"Route read queries to a PostgreSQL replica by creating a second AsyncEngine pointing at the replica host, then override get_bind() in a Session subclass used via AsyncSession(sync_session_class=...) to dispatch SELECT statements to the replica engine and all writes to the primary. This is the production-safe approach in Dynamic Schema and Multi-Tenant Routing, replacing the removed SQLAlchemy 1.4 Session(binds=...) API.","md",{"date":2923},"2026-06-18","\u002Fadvanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Frouting-reads-to-replicas-with-async-engines",{"title":5,"description":2920},"advanced-query-patterns-and-bulk-data-operations\u002Fdynamic-schema-and-multi-tenant-routing\u002Frouting-reads-to-replicas-with-async-engines\u002Findex","8cpMvWK8mUHC5pQiN5YqZguMAK61H4fSSUZ8bZhsug8",1781810028979]