[{"data":1,"prerenderedAt":1534},["ShallowReactive",2],{"page-\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Fhandling-asyncpg-prepared-statement-errors-with-pgbouncer\u002F":3},{"id":4,"title":5,"body":6,"description":1526,"extension":1527,"meta":1528,"navigation":90,"path":1530,"seo":1531,"stem":1532,"__hash__":1533},"content\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Fhandling-asyncpg-prepared-statement-errors-with-pgbouncer\u002Findex.md","Handling asyncpg Prepared Statement Errors with PgBouncer",{"type":7,"value":8,"toc":1510},"minimark",[9,13,36,41,44,148,266,277,281,286,305,308,312,325,331,339,342,361,367,371,731,748,752,915,919,923,931,934,980,983,999,1002,1014,1018,1031,1189,1195,1199,1202,1394,1401,1405,1429,1435,1446,1461,1465,1506],[10,11,5],"h1",{"id":12},"handling-asyncpg-prepared-statement-errors-with-pgbouncer",[14,15,16,17,21,22,25,26,29,30,35],"p",{},"Set ",[18,19,20],"code",{},"statement_cache_size=0"," and ",[18,23,24],{},"prepared_statement_cache_size=0"," in your engine's ",[18,27,28],{},"connect_args"," — this disables asyncpg's prepared statement cache and eliminates all PgBouncer transaction-mode incompatibility errors, as detailed in the ",[31,32,34],"a",{"href":33},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002F","dialect-specific gotchas and driver quirks guide",".",[37,38,40],"h2",{"id":39},"quick-answer","Quick Answer",[14,42,43],{},"The moment you introduce PgBouncer in transaction pooling mode between your application and PostgreSQL, asyncpg's default prepared statement cache becomes a liability. Here is the before\u002Fafter fix:",[45,46,51],"pre",{"className":47,"code":48,"language":49,"meta":50,"style":50},"language-python shiki shiki-themes github-light github-dark","# Before — asyncpg with default prepared statement cache\n# Raises InvalidSQLStatementNameError behind PgBouncer transaction pooling\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\",\n    pool_size=10,\n    max_overflow=20,\n)\n","python","",[18,52,53,62,68,85,92,104,114,129,142],{"__ignoreMap":50},[54,55,58],"span",{"class":56,"line":57},"line",1,[54,59,61],{"class":60},"sJ8bj","# Before — asyncpg with default prepared statement cache\n",[54,63,65],{"class":56,"line":64},2,[54,66,67],{"class":60},"# Raises InvalidSQLStatementNameError behind PgBouncer transaction pooling\n",[54,69,71,75,79,82],{"class":56,"line":70},3,[54,72,74],{"class":73},"szBVR","from",[54,76,78],{"class":77},"sVt8B"," sqlalchemy.ext.asyncio ",[54,80,81],{"class":73},"import",[54,83,84],{"class":77}," create_async_engine\n",[54,86,88],{"class":56,"line":87},4,[54,89,91],{"emptyLinePlaceholder":90},true,"\n",[54,93,95,98,101],{"class":56,"line":94},5,[54,96,97],{"class":77},"engine ",[54,99,100],{"class":73},"=",[54,102,103],{"class":77}," create_async_engine(\n",[54,105,107,111],{"class":56,"line":106},6,[54,108,110],{"class":109},"sZZnC","    \"postgresql+asyncpg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\"",[54,112,113],{"class":77},",\n",[54,115,117,121,123,127],{"class":56,"line":116},7,[54,118,120],{"class":119},"s4XuR","    pool_size",[54,122,100],{"class":73},[54,124,126],{"class":125},"sj4cs","10",[54,128,113],{"class":77},[54,130,132,135,137,140],{"class":56,"line":131},8,[54,133,134],{"class":119},"    max_overflow",[54,136,100],{"class":73},[54,138,139],{"class":125},"20",[54,141,113],{"class":77},[54,143,145],{"class":56,"line":144},9,[54,146,147],{"class":77},")\n",[45,149,151],{"className":47,"code":150,"language":49,"meta":50,"style":50},"# After — asyncpg with prepared statement cache disabled\n# Works correctly behind PgBouncer in any pooling mode\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@pgbouncer:6432\u002Fmydb\",\n    pool_size=10,\n    max_overflow=20,\n    connect_args={\n        \"statement_cache_size\": 0,           # disable client-side LRU cache\n        \"prepared_statement_cache_size\": 0,  # asyncpg ≥ 0.28 second cache key\n    },\n)\n",[18,152,153,158,163,173,177,185,191,201,211,221,239,255,261],{"__ignoreMap":50},[54,154,155],{"class":56,"line":57},[54,156,157],{"class":60},"# After — asyncpg with prepared statement cache disabled\n",[54,159,160],{"class":56,"line":64},[54,161,162],{"class":60},"# Works correctly behind PgBouncer in any pooling mode\n",[54,164,165,167,169,171],{"class":56,"line":70},[54,166,74],{"class":73},[54,168,78],{"class":77},[54,170,81],{"class":73},[54,172,84],{"class":77},[54,174,175],{"class":56,"line":87},[54,176,91],{"emptyLinePlaceholder":90},[54,178,179,181,183],{"class":56,"line":94},[54,180,97],{"class":77},[54,182,100],{"class":73},[54,184,103],{"class":77},[54,186,187,189],{"class":56,"line":106},[54,188,110],{"class":109},[54,190,113],{"class":77},[54,192,193,195,197,199],{"class":56,"line":116},[54,194,120],{"class":119},[54,196,100],{"class":73},[54,198,126],{"class":125},[54,200,113],{"class":77},[54,202,203,205,207,209],{"class":56,"line":131},[54,204,134],{"class":119},[54,206,100],{"class":73},[54,208,139],{"class":125},[54,210,113],{"class":77},[54,212,213,216,218],{"class":56,"line":144},[54,214,215],{"class":119},"    connect_args",[54,217,100],{"class":73},[54,219,220],{"class":77},"{\n",[54,222,224,227,230,233,236],{"class":56,"line":223},10,[54,225,226],{"class":109},"        \"statement_cache_size\"",[54,228,229],{"class":77},": ",[54,231,232],{"class":125},"0",[54,234,235],{"class":77},",           ",[54,237,238],{"class":60},"# disable client-side LRU cache\n",[54,240,242,245,247,249,252],{"class":56,"line":241},11,[54,243,244],{"class":109},"        \"prepared_statement_cache_size\"",[54,246,229],{"class":77},[54,248,232],{"class":125},[54,250,251],{"class":77},",  ",[54,253,254],{"class":60},"# asyncpg ≥ 0.28 second cache key\n",[54,256,258],{"class":56,"line":257},12,[54,259,260],{"class":77},"    },\n",[54,262,264],{"class":56,"line":263},13,[54,265,147],{"class":77},[14,267,268,269,272,273,276],{},"Both keys are required. ",[18,270,271],{},"statement_cache_size"," has existed since asyncpg's early versions; ",[18,274,275],{},"prepared_statement_cache_size"," was introduced in asyncpg 0.28 as a separate control for a refactored cache layer. Setting only one leaves the other active, which will surface the same errors on asyncpg 0.28 and later.",[37,278,280],{"id":279},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[282,283,285],"h3",{"id":284},"why-asyncpg-caches-prepared-statements","Why asyncpg Caches Prepared Statements",[14,287,288,289,292,293,296,297,300,301,304],{},"When asyncpg executes a query for the first time on a connection, it sends a PostgreSQL wire protocol ",[18,290,291],{},"Parse"," message. PostgreSQL responds with a ",[18,294,295],{},"ParseComplete"," and registers the statement under a generated name like ",[18,298,299],{},"__asyncpg_stmt_0__",". Subsequent executions of the same query text on the same connection send a ",[18,302,303],{},"Bind"," message referencing that name, skipping the parse and plan phases entirely.",[14,306,307],{},"This is a genuine performance win on a dedicated connection: parsing and planning can account for 10–30% of total query latency on short OLTP queries. asyncpg's cache is a direct application of this optimisation.",[282,309,311],{"id":310},"why-transaction-pooling-breaks-it","Why Transaction Pooling Breaks It",[14,313,314,315,318,319,322,323,35],{},"PgBouncer's transaction pooling mode assigns a backend PostgreSQL connection to a client only for the duration of a single transaction. Once your ",[18,316,317],{},"COMMIT"," or ",[18,320,321],{},"ROLLBACK"," fires, PgBouncer is free to route your next transaction to a completely different backend server — one that has never seen ",[18,324,299],{},[14,326,327,328,330],{},"When your next query arrives and asyncpg sends a ",[18,329,303],{}," message referencing that name, the new backend returns an error:",[45,332,337],{"className":333,"code":335,"language":336},[334],"language-text","asyncpg.exceptions.InvalidSQLStatementNameError:\nprepared statement \"__asyncpg_stmt_0__\" does not exist\n","text",[18,338,335],{"__ignoreMap":50},[14,340,341],{},"The sequence is:",[343,344,345,352,355],"ol",{},[346,347,348,349,351],"li",{},"Connection A → Backend-1: Parse ",[18,350,299],{}," (SELECT * FROM orders WHERE id = $1)",[346,353,354],{},"Transaction commits → PgBouncer releases Backend-1",[346,356,357,358,360],{},"Connection A → Backend-7 (new backend): Bind ",[18,359,299],{}," → ERROR",[14,362,363,364,366],{},"Because asyncpg's cache stores the name but not the query text, it cannot transparently re-issue the ",[18,365,291],{}," on the new backend. SQLAlchemy does not intercept this at the pool level by default.",[282,368,370],{"id":369},"the-full-fix-with-async-engine-configuration","The Full Fix with Async Engine Configuration",[45,372,374],{"className":47,"code":373,"language":49,"meta":50,"style":50},"# Full async engine setup for PgBouncer transaction pooling\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp_user:secret@pgbouncer.internal:6432\u002Fproduction\",\n    # Pool settings: these are SQLAlchemy pool settings, not PgBouncer settings\n    pool_size=10,           # concurrent connections from this process\n    max_overflow=20,        # burst connections above pool_size\n    pool_timeout=30,        # seconds to wait for a connection from pool\n    pool_recycle=1800,      # recycle connections every 30 min (avoid stale TCP)\n    pool_pre_ping=True,     # validate connection before checkout\n    # Driver-level settings: passed verbatim to asyncpg.connect()\n    connect_args={\n        \"statement_cache_size\": 0,\n        \"prepared_statement_cache_size\": 0,\n        # Optional but recommended: set application_name for pg_stat_activity\n        \"server_settings\": {\n            \"application_name\": \"order_service\",\n        },\n    },\n)\n\nAsyncSessionLocal = async_sessionmaker(\n    engine,\n    class_=AsyncSession,\n    expire_on_commit=False,\n)\n\n# Usage — identical whether or not PgBouncer is in front\nasync def get_pending_orders(tenant_id: int) -> list[Order]:\n    async with AsyncSessionLocal() as session:\n        result = await session.execute(\n            select(Order)\n            .where(Order.tenant_id == tenant_id, Order.status == \"pending\")\n            .order_by(Order.created_at.desc())\n        )\n        return result.scalars().all()\n",[18,375,376,381,392,396,404,411,416,429,443,458,474,490,495,503,514,525,531,540,553,559,564,569,574,585,591,602,615,620,625,631,653,671,685,691,710,716,722],{"__ignoreMap":50},[54,377,378],{"class":56,"line":57},[54,379,380],{"class":60},"# Full async engine setup for PgBouncer transaction pooling\n",[54,382,383,385,387,389],{"class":56,"line":64},[54,384,74],{"class":73},[54,386,78],{"class":77},[54,388,81],{"class":73},[54,390,391],{"class":77}," create_async_engine, AsyncSession, async_sessionmaker\n",[54,393,394],{"class":56,"line":70},[54,395,91],{"emptyLinePlaceholder":90},[54,397,398,400,402],{"class":56,"line":87},[54,399,97],{"class":77},[54,401,100],{"class":73},[54,403,103],{"class":77},[54,405,406,409],{"class":56,"line":94},[54,407,408],{"class":109},"    \"postgresql+asyncpg:\u002F\u002Fapp_user:secret@pgbouncer.internal:6432\u002Fproduction\"",[54,410,113],{"class":77},[54,412,413],{"class":56,"line":106},[54,414,415],{"class":60},"    # Pool settings: these are SQLAlchemy pool settings, not PgBouncer settings\n",[54,417,418,420,422,424,426],{"class":56,"line":116},[54,419,120],{"class":119},[54,421,100],{"class":73},[54,423,126],{"class":125},[54,425,235],{"class":77},[54,427,428],{"class":60},"# concurrent connections from this process\n",[54,430,431,433,435,437,440],{"class":56,"line":131},[54,432,134],{"class":119},[54,434,100],{"class":73},[54,436,139],{"class":125},[54,438,439],{"class":77},",        ",[54,441,442],{"class":60},"# burst connections above pool_size\n",[54,444,445,448,450,453,455],{"class":56,"line":144},[54,446,447],{"class":119},"    pool_timeout",[54,449,100],{"class":73},[54,451,452],{"class":125},"30",[54,454,439],{"class":77},[54,456,457],{"class":60},"# seconds to wait for a connection from pool\n",[54,459,460,463,465,468,471],{"class":56,"line":223},[54,461,462],{"class":119},"    pool_recycle",[54,464,100],{"class":73},[54,466,467],{"class":125},"1800",[54,469,470],{"class":77},",      ",[54,472,473],{"class":60},"# recycle connections every 30 min (avoid stale TCP)\n",[54,475,476,479,481,484,487],{"class":56,"line":241},[54,477,478],{"class":119},"    pool_pre_ping",[54,480,100],{"class":73},[54,482,483],{"class":125},"True",[54,485,486],{"class":77},",     ",[54,488,489],{"class":60},"# validate connection before checkout\n",[54,491,492],{"class":56,"line":257},[54,493,494],{"class":60},"    # Driver-level settings: passed verbatim to asyncpg.connect()\n",[54,496,497,499,501],{"class":56,"line":263},[54,498,215],{"class":119},[54,500,100],{"class":73},[54,502,220],{"class":77},[54,504,506,508,510,512],{"class":56,"line":505},14,[54,507,226],{"class":109},[54,509,229],{"class":77},[54,511,232],{"class":125},[54,513,113],{"class":77},[54,515,517,519,521,523],{"class":56,"line":516},15,[54,518,244],{"class":109},[54,520,229],{"class":77},[54,522,232],{"class":125},[54,524,113],{"class":77},[54,526,528],{"class":56,"line":527},16,[54,529,530],{"class":60},"        # Optional but recommended: set application_name for pg_stat_activity\n",[54,532,534,537],{"class":56,"line":533},17,[54,535,536],{"class":109},"        \"server_settings\"",[54,538,539],{"class":77},": {\n",[54,541,543,546,548,551],{"class":56,"line":542},18,[54,544,545],{"class":109},"            \"application_name\"",[54,547,229],{"class":77},[54,549,550],{"class":109},"\"order_service\"",[54,552,113],{"class":77},[54,554,556],{"class":56,"line":555},19,[54,557,558],{"class":77},"        },\n",[54,560,562],{"class":56,"line":561},20,[54,563,260],{"class":77},[54,565,567],{"class":56,"line":566},21,[54,568,147],{"class":77},[54,570,572],{"class":56,"line":571},22,[54,573,91],{"emptyLinePlaceholder":90},[54,575,577,580,582],{"class":56,"line":576},23,[54,578,579],{"class":77},"AsyncSessionLocal ",[54,581,100],{"class":73},[54,583,584],{"class":77}," async_sessionmaker(\n",[54,586,588],{"class":56,"line":587},24,[54,589,590],{"class":77},"    engine,\n",[54,592,594,597,599],{"class":56,"line":593},25,[54,595,596],{"class":119},"    class_",[54,598,100],{"class":73},[54,600,601],{"class":77},"AsyncSession,\n",[54,603,605,608,610,613],{"class":56,"line":604},26,[54,606,607],{"class":119},"    expire_on_commit",[54,609,100],{"class":73},[54,611,612],{"class":125},"False",[54,614,113],{"class":77},[54,616,618],{"class":56,"line":617},27,[54,619,147],{"class":77},[54,621,623],{"class":56,"line":622},28,[54,624,91],{"emptyLinePlaceholder":90},[54,626,628],{"class":56,"line":627},29,[54,629,630],{"class":60},"# Usage — identical whether or not PgBouncer is in front\n",[54,632,634,637,640,644,647,650],{"class":56,"line":633},30,[54,635,636],{"class":73},"async",[54,638,639],{"class":73}," def",[54,641,643],{"class":642},"sScJk"," get_pending_orders",[54,645,646],{"class":77},"(tenant_id: ",[54,648,649],{"class":125},"int",[54,651,652],{"class":77},") -> list[Order]:\n",[54,654,656,659,662,665,668],{"class":56,"line":655},31,[54,657,658],{"class":73},"    async",[54,660,661],{"class":73}," with",[54,663,664],{"class":77}," AsyncSessionLocal() ",[54,666,667],{"class":73},"as",[54,669,670],{"class":77}," session:\n",[54,672,674,677,679,682],{"class":56,"line":673},32,[54,675,676],{"class":77},"        result ",[54,678,100],{"class":73},[54,680,681],{"class":73}," await",[54,683,684],{"class":77}," session.execute(\n",[54,686,688],{"class":56,"line":687},33,[54,689,690],{"class":77},"            select(Order)\n",[54,692,694,697,700,703,705,708],{"class":56,"line":693},34,[54,695,696],{"class":77},"            .where(Order.tenant_id ",[54,698,699],{"class":73},"==",[54,701,702],{"class":77}," tenant_id, Order.status ",[54,704,699],{"class":73},[54,706,707],{"class":109}," \"pending\"",[54,709,147],{"class":77},[54,711,713],{"class":56,"line":712},35,[54,714,715],{"class":77},"            .order_by(Order.created_at.desc())\n",[54,717,719],{"class":56,"line":718},36,[54,720,721],{"class":77},"        )\n",[54,723,725,728],{"class":56,"line":724},37,[54,726,727],{"class":73},"        return",[54,729,730],{"class":77}," result.scalars().all()\n",[14,732,733,736,737,740,741,318,744,747],{},[18,734,735],{},"pool_pre_ping=True"," is especially important behind PgBouncer: it issues a ",[18,738,739],{},"SELECT 1"," before returning a connection from the pool, catching connections that have been closed by PgBouncer's ",[18,742,743],{},"server_idle_timeout",[18,745,746],{},"server_lifetime"," settings.",[37,749,751],{"id":750},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[753,754,755,771],"table",{},[756,757,758],"thead",{},[759,760,761,765,768],"tr",{},[762,763,764],"th",{},"Error \u002F Symptom",[762,766,767],{},"Root Cause",[762,769,770],{},"Fix",[772,773,774,797,813,838,863,880],"tbody",{},[759,775,776,782,788],{},[777,778,779],"td",{},[18,780,781],{},"asyncpg.exceptions.InvalidSQLStatementNameError: prepared statement \"__asyncpg_stmt_0__\" does not exist",[777,783,784,785,787],{},"asyncpg sent a ",[18,786,303],{}," referencing a prepared statement that does not exist on the current backend (PgBouncer rotated to a new server)",[777,789,16,790,21,792,794,795],{},[18,791,20],{},[18,793,24],{}," in ",[18,796,28],{},[759,798,799,804,810],{},[777,800,801],{},[18,802,803],{},"asyncpg.exceptions.DuplicatePreparedStatementError: prepared statement \"__asyncpg_stmt_0__\" already exists",[777,805,806,807,809],{},"asyncpg attempted to re-",[18,808,291],{}," a statement name that already exists on this backend, likely due to a reconnect after an error without flushing the client cache",[777,811,812],{},"Same fix: disable the cache entirely; the duplicate occurs because asyncpg reuses names without checking backend state",[759,814,815,820,827],{},[777,816,817],{},[18,818,819],{},"sqlalchemy.exc.ProgrammingError: (asyncpg.exceptions.ProtocolError) cannot insert multiple commands into a prepared statement",[777,821,822,823,826],{},"A ",[18,824,825],{},"text()"," statement contains a semicolon (multiple commands) — asyncpg always uses prepared statements for text() even for multi-statement strings",[777,828,829,830,833,834,837],{},"Split into separate ",[18,831,832],{},"session.execute()"," calls, or use ",[18,835,836],{},"conn.exec_driver_sql()"," which bypasses prepared-statement mode",[759,839,840,848,851],{},[777,841,842,845,846],{},[18,843,844],{},"sqlalchemy.exc.OperationalError: server closed the connection unexpectedly"," after PgBouncer ",[18,847,743],{},[777,849,850],{},"PgBouncer closed an idle backend connection; SQLAlchemy's pool still holds the client connection object, which is now broken",[777,852,853,854,856,857,860,861],{},"Enable ",[18,855,735],{}," on the engine; tune ",[18,858,859],{},"pool_recycle"," to be less than PgBouncer's ",[18,862,746],{},[759,864,865,871,874],{},[777,866,867,870],{},[18,868,869],{},"asyncpg"," errors appear only under load, not in development",[777,872,873],{},"Development uses a direct connection (no PgBouncer); the cache works fine there. Under load, PgBouncer's connection rotation exposes the cache mismatch",[777,875,876,877,879],{},"Always use ",[18,878,20],{}," in all environments where PgBouncer may be in the path, including staging",[759,881,882,887,901],{},[777,883,884],{},[18,885,886],{},"asyncpg.exceptions.TooManyConnectionsError",[777,888,889,890,893,894,897,898],{},"Application ",[18,891,892],{},"pool_size + max_overflow"," × number of processes exceeds PgBouncer's ",[18,895,896],{},"max_client_conn"," or PostgreSQL's ",[18,899,900],{},"max_connections",[777,902,903,904,907,908,911,912,914],{},"Reduce ",[18,905,906],{},"pool_size",", reduce ",[18,909,910],{},"max_overflow",", or increase PgBouncer's ",[18,913,906],{}," for the database",[37,916,918],{"id":917},"advanced-prepared-statement-error-mitigation","Advanced Prepared Statement Error Mitigation",[282,920,922],{"id":921},"session-pooling-as-an-alternative","Session Pooling as an Alternative",[14,924,925,926,930],{},"If disabling the prepared statement cache causes measurable query latency regression (which it rarely does for OLTP workloads, but may matter for analytical queries), consider switching PgBouncer to ",[927,928,929],"strong",{},"session pooling mode"," instead of transaction pooling mode.",[14,932,933],{},"In session pooling, a client gets a dedicated backend connection for the entire session lifetime — prepared statements persist correctly. The trade-off is that PgBouncer provides less multiplexing benefit, requiring more backend connections:",[45,935,939],{"className":936,"code":937,"language":938,"meta":50,"style":50},"language-ini shiki shiki-themes github-light github-dark","# pgbouncer.ini — session pooling mode (no asyncpg changes needed)\n[databases]\nmydb = host=postgres.internal dbname=mydb\n\n[pgbouncer]\npool_mode = session        # was: transaction\nmax_client_conn = 1000\ndefault_pool_size = 50\n","ini",[18,940,941,946,951,956,960,965,970,975],{"__ignoreMap":50},[54,942,943],{"class":56,"line":57},[54,944,945],{},"# pgbouncer.ini — session pooling mode (no asyncpg changes needed)\n",[54,947,948],{"class":56,"line":64},[54,949,950],{},"[databases]\n",[54,952,953],{"class":56,"line":70},[54,954,955],{},"mydb = host=postgres.internal dbname=mydb\n",[54,957,958],{"class":56,"line":87},[54,959,91],{"emptyLinePlaceholder":90},[54,961,962],{"class":56,"line":94},[54,963,964],{},"[pgbouncer]\n",[54,966,967],{"class":56,"line":106},[54,968,969],{},"pool_mode = session        # was: transaction\n",[54,971,972],{"class":56,"line":116},[54,973,974],{},"max_client_conn = 1000\n",[54,976,977],{"class":56,"line":131},[54,978,979],{},"default_pool_size = 50\n",[14,981,982],{},"Session pooling is appropriate when:",[984,985,986,989,992],"ul",{},[346,987,988],{},"Your workload issues many queries per session and benefits from the parse cache",[346,990,991],{},"You can afford more PostgreSQL backend connections (each client maps to one backend for session duration)",[346,993,994,995,998],{},"Your sessions are short-lived (e.g., HTTP request handlers that use ",[18,996,997],{},"async with AsyncSession() as session",")",[14,1000,1001],{},"Transaction pooling is appropriate when:",[984,1003,1004,1009],{},[346,1005,1006,1007],{},"You need to multiplex thousands of application connections onto a small PostgreSQL ",[18,1008,900],{},[346,1010,1011,1012,998],{},"Prepared statement caching is disabled (",[18,1013,20],{},[282,1015,1017],{"id":1016},"verifying-the-fix-with-pg_stat_statements","Verifying the Fix with pg_stat_statements",[14,1019,1020,1021,1023,1024,1026,1027,1030],{},"After deploying ",[18,1022,20],{},", confirm that asyncpg no longer sends ",[18,1025,291],{}," messages by querying ",[18,1028,1029],{},"pg_stat_statements",":",[45,1032,1034],{"className":47,"code":1033,"language":49,"meta":50,"style":50},"# Verify no prepared statements are being created\nfrom sqlalchemy import text\n\nasync def check_prepared_statements(session: AsyncSession) -> list[dict]:\n    result = await session.execute(\n        text(\"\"\"\n            SELECT\n                name,\n                statement,\n                prepare_time\n            FROM pg_prepared_statements\n            ORDER BY prepare_time DESC\n            LIMIT 20\n        \"\"\")\n    )\n    rows = result.fetchall()\n    return [{\"name\": r.name, \"statement\": r.statement[:80]} for r in rows]\n",[18,1035,1036,1041,1053,1057,1075,1086,1094,1099,1104,1109,1114,1119,1124,1129,1136,1141,1151],{"__ignoreMap":50},[54,1037,1038],{"class":56,"line":57},[54,1039,1040],{"class":60},"# Verify no prepared statements are being created\n",[54,1042,1043,1045,1048,1050],{"class":56,"line":64},[54,1044,74],{"class":73},[54,1046,1047],{"class":77}," sqlalchemy ",[54,1049,81],{"class":73},[54,1051,1052],{"class":77}," text\n",[54,1054,1055],{"class":56,"line":70},[54,1056,91],{"emptyLinePlaceholder":90},[54,1058,1059,1061,1063,1066,1069,1072],{"class":56,"line":87},[54,1060,636],{"class":73},[54,1062,639],{"class":73},[54,1064,1065],{"class":642}," check_prepared_statements",[54,1067,1068],{"class":77},"(session: AsyncSession) -> list[",[54,1070,1071],{"class":125},"dict",[54,1073,1074],{"class":77},"]:\n",[54,1076,1077,1080,1082,1084],{"class":56,"line":94},[54,1078,1079],{"class":77},"    result ",[54,1081,100],{"class":73},[54,1083,681],{"class":73},[54,1085,684],{"class":77},[54,1087,1088,1091],{"class":56,"line":106},[54,1089,1090],{"class":77},"        text(",[54,1092,1093],{"class":109},"\"\"\"\n",[54,1095,1096],{"class":56,"line":116},[54,1097,1098],{"class":109},"            SELECT\n",[54,1100,1101],{"class":56,"line":131},[54,1102,1103],{"class":109},"                name,\n",[54,1105,1106],{"class":56,"line":144},[54,1107,1108],{"class":109},"                statement,\n",[54,1110,1111],{"class":56,"line":223},[54,1112,1113],{"class":109},"                prepare_time\n",[54,1115,1116],{"class":56,"line":241},[54,1117,1118],{"class":109},"            FROM pg_prepared_statements\n",[54,1120,1121],{"class":56,"line":257},[54,1122,1123],{"class":109},"            ORDER BY prepare_time DESC\n",[54,1125,1126],{"class":56,"line":263},[54,1127,1128],{"class":109},"            LIMIT 20\n",[54,1130,1131,1134],{"class":56,"line":505},[54,1132,1133],{"class":109},"        \"\"\"",[54,1135,147],{"class":77},[54,1137,1138],{"class":56,"line":516},[54,1139,1140],{"class":77},"    )\n",[54,1142,1143,1146,1148],{"class":56,"line":527},[54,1144,1145],{"class":77},"    rows ",[54,1147,100],{"class":73},[54,1149,1150],{"class":77}," result.fetchall()\n",[54,1152,1153,1156,1159,1162,1165,1168,1171,1174,1177,1180,1183,1186],{"class":56,"line":533},[54,1154,1155],{"class":73},"    return",[54,1157,1158],{"class":77}," [{",[54,1160,1161],{"class":109},"\"name\"",[54,1163,1164],{"class":77},": r.name, ",[54,1166,1167],{"class":109},"\"statement\"",[54,1169,1170],{"class":77},": r.statement[:",[54,1172,1173],{"class":125},"80",[54,1175,1176],{"class":77},"]} ",[54,1178,1179],{"class":73},"for",[54,1181,1182],{"class":77}," r ",[54,1184,1185],{"class":73},"in",[54,1187,1188],{"class":77}," rows]\n",[14,1190,1191,1192,1194],{},"With ",[18,1193,20],{},", this query should return zero rows from your application's connection. Any remaining rows are from other clients (e.g., admin tools, migration runners) that do not go through asyncpg.",[282,1196,1198],{"id":1197},"detecting-pgbouncer-in-the-connection-path","Detecting PgBouncer in the Connection Path",[14,1200,1201],{},"If you are uncertain whether PgBouncer is in your connection path (e.g., in cloud-managed databases like Cloud SQL Proxy or RDS Proxy):",[45,1203,1205],{"className":47,"code":1204,"language":49,"meta":50,"style":50},"# Detect PgBouncer by checking for its signature GUC\nfrom sqlalchemy import text\n\nasync def is_behind_pgbouncer(session: AsyncSession) -> bool:\n    try:\n        result = await session.execute(text(\"SHOW server_version\"))\n        version = result.scalar()\n        # PgBouncer returns its own version string, not PostgreSQL's\n        return \"PgBouncer\" in (version or \"\")\n    except Exception:\n        return False\n\nasync def check_pool_mode(session: AsyncSession) -> str | None:\n    try:\n        result = await session.execute(text(\"SHOW pool_mode\"))\n        return result.scalar()\n    except Exception:\n        # SHOW pool_mode is a PgBouncer command; raises on real Postgres\n        return None\n",[18,1206,1207,1212,1222,1226,1244,1251,1268,1278,1283,1304,1314,1321,1325,1347,1353,1368,1374,1382,1387],{"__ignoreMap":50},[54,1208,1209],{"class":56,"line":57},[54,1210,1211],{"class":60},"# Detect PgBouncer by checking for its signature GUC\n",[54,1213,1214,1216,1218,1220],{"class":56,"line":64},[54,1215,74],{"class":73},[54,1217,1047],{"class":77},[54,1219,81],{"class":73},[54,1221,1052],{"class":77},[54,1223,1224],{"class":56,"line":70},[54,1225,91],{"emptyLinePlaceholder":90},[54,1227,1228,1230,1232,1235,1238,1241],{"class":56,"line":87},[54,1229,636],{"class":73},[54,1231,639],{"class":73},[54,1233,1234],{"class":642}," is_behind_pgbouncer",[54,1236,1237],{"class":77},"(session: AsyncSession) -> ",[54,1239,1240],{"class":125},"bool",[54,1242,1243],{"class":77},":\n",[54,1245,1246,1249],{"class":56,"line":94},[54,1247,1248],{"class":73},"    try",[54,1250,1243],{"class":77},[54,1252,1253,1255,1257,1259,1262,1265],{"class":56,"line":106},[54,1254,676],{"class":77},[54,1256,100],{"class":73},[54,1258,681],{"class":73},[54,1260,1261],{"class":77}," session.execute(text(",[54,1263,1264],{"class":109},"\"SHOW server_version\"",[54,1266,1267],{"class":77},"))\n",[54,1269,1270,1273,1275],{"class":56,"line":116},[54,1271,1272],{"class":77},"        version ",[54,1274,100],{"class":73},[54,1276,1277],{"class":77}," result.scalar()\n",[54,1279,1280],{"class":56,"line":131},[54,1281,1282],{"class":60},"        # PgBouncer returns its own version string, not PostgreSQL's\n",[54,1284,1285,1287,1290,1293,1296,1299,1302],{"class":56,"line":144},[54,1286,727],{"class":73},[54,1288,1289],{"class":109}," \"PgBouncer\"",[54,1291,1292],{"class":73}," in",[54,1294,1295],{"class":77}," (version ",[54,1297,1298],{"class":73},"or",[54,1300,1301],{"class":109}," \"\"",[54,1303,147],{"class":77},[54,1305,1306,1309,1312],{"class":56,"line":223},[54,1307,1308],{"class":73},"    except",[54,1310,1311],{"class":125}," Exception",[54,1313,1243],{"class":77},[54,1315,1316,1318],{"class":56,"line":241},[54,1317,727],{"class":73},[54,1319,1320],{"class":125}," False\n",[54,1322,1323],{"class":56,"line":257},[54,1324,91],{"emptyLinePlaceholder":90},[54,1326,1327,1329,1331,1334,1336,1339,1342,1345],{"class":56,"line":263},[54,1328,636],{"class":73},[54,1330,639],{"class":73},[54,1332,1333],{"class":642}," check_pool_mode",[54,1335,1237],{"class":77},[54,1337,1338],{"class":125},"str",[54,1340,1341],{"class":73}," |",[54,1343,1344],{"class":125}," None",[54,1346,1243],{"class":77},[54,1348,1349,1351],{"class":56,"line":505},[54,1350,1248],{"class":73},[54,1352,1243],{"class":77},[54,1354,1355,1357,1359,1361,1363,1366],{"class":56,"line":516},[54,1356,676],{"class":77},[54,1358,100],{"class":73},[54,1360,681],{"class":73},[54,1362,1261],{"class":77},[54,1364,1365],{"class":109},"\"SHOW pool_mode\"",[54,1367,1267],{"class":77},[54,1369,1370,1372],{"class":56,"line":527},[54,1371,727],{"class":73},[54,1373,1277],{"class":77},[54,1375,1376,1378,1380],{"class":56,"line":533},[54,1377,1308],{"class":73},[54,1379,1311],{"class":125},[54,1381,1243],{"class":77},[54,1383,1384],{"class":56,"line":542},[54,1385,1386],{"class":60},"        # SHOW pool_mode is a PgBouncer command; raises on real Postgres\n",[54,1388,1389,1391],{"class":56,"line":555},[54,1390,727],{"class":73},[54,1392,1393],{"class":125}," None\n",[14,1395,1396,1397,1400],{},"As a rule: if there is any proxy, pooler, or cloud SQL connector between your application and PostgreSQL, treat it as PgBouncer transaction mode and disable prepared statement caching. The performance cost of disabling the cache is negligible compared to the operational cost of debugging ",[18,1398,1399],{},"InvalidSQLStatementNameError"," in production.",[37,1402,1404],{"id":1403},"frequently-asked-questions","Frequently Asked Questions",[14,1406,1407,1415,1416,1418,1419,1421,1422,1424,1425,1428],{},[927,1408,1409,1410,21,1412,1414],{},"Do I need to set both ",[18,1411,271],{},[18,1413,275],{},", or just one?","\nBoth. On asyncpg \u003C 0.28, only ",[18,1417,271],{}," exists and controls the cache. On asyncpg ≥ 0.28, both controls exist independently and both default to non-zero values. Setting only ",[18,1420,20],{}," on asyncpg 0.28+ still leaves ",[18,1423,275],{}," active, which can cause ",[18,1426,1427],{},"DuplicatePreparedStatementError",". Set both to 0 unconditionally.",[14,1430,1431,1434],{},[927,1432,1433],{},"Will disabling the prepared statement cache make my application slower?","\nRarely measurably for OLTP workloads. The parse\u002Fplan overhead per query is typically 0.1–2 ms for simple indexed queries. asyncpg's cache saves this per-connection (not per-request, since SQLAlchemy pools reuse connections), so the practical saving only materialises for connections that execute the same query hundreds of times without reconnecting. Behind PgBouncer transaction pooling, connections are not reused in that pattern anyway.",[14,1436,1437,1440,1441,21,1443,1445],{},[927,1438,1439],{},"Does RDS Proxy have the same problem as PgBouncer?","\nYes. RDS Proxy in transaction mode (the default for most configurations) multiplexes connections in the same way as PgBouncer transaction pooling. Apply the same fix: ",[18,1442,20],{},[18,1444,24],{},". AWS's own documentation recommends disabling prepared statement pinning or switching RDS Proxy to session pinning mode for asyncpg.",[14,1447,1448,1454,1455,1457,1458,1460],{},[927,1449,1450,1451,1453],{},"Can I catch and retry ",[18,1452,1399],{}," automatically instead of disabling the cache?","\nTechnically yes, but this is fragile. asyncpg's error occurs at the wire protocol level, below SQLAlchemy's retry machinery. Implementing a retry requires wrapping every database call in a custom retry loop that detects ",[18,1456,1399],{},", clears the connection from the pool, and retries — which is exactly what ",[18,1459,20],{}," eliminates without the complexity.",[37,1462,1464],{"id":1463},"related","Related",[984,1466,1467,1473,1480,1499],{},[346,1468,1469,1472],{},[31,1470,1471],{"href":33},"Dialect-Specific Gotchas and Driver Quirks"," — full comparison of asyncpg, psycopg3, aiomysql, and aiosqlite quirks including type codecs, RETURNING support, and isolation defaults.",[346,1474,1475,1479],{},[31,1476,1478],{"href":1477},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fchoosing-between-asyncpg-and-psycopg-async-drivers\u002F","Choosing Between asyncpg and psycopg Async Drivers"," — if the prepared statement cache is a dealbreaker, psycopg3 with text protocol is PgBouncer-compatible by default.",[346,1481,1482,1486,1487,1489,1490,1489,1492,1495,1496,1498],{},[31,1483,1485],{"href":1484},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools"," — ",[18,1488,906],{},", ",[18,1491,910],{},[18,1493,1494],{},"pool_pre_ping",", and ",[18,1497,859],{}," settings that complement PgBouncer configuration.",[346,1500,1501,1505],{},[31,1502,1504],{"href":1503},"\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002F","Tuning Connection Pools for Cloud Databases"," — RDS Proxy, Cloud SQL Auth Proxy, and AlloyDB Omni connection tuning with PgBouncer-aware settings.",[1507,1508,1509],"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":50,"searchDepth":64,"depth":64,"links":1511},[1512,1513,1518,1519,1524,1525],{"id":39,"depth":64,"text":40},{"id":279,"depth":64,"text":280,"children":1514},[1515,1516,1517],{"id":284,"depth":70,"text":285},{"id":310,"depth":70,"text":311},{"id":369,"depth":70,"text":370},{"id":750,"depth":64,"text":751},{"id":917,"depth":64,"text":918,"children":1520},[1521,1522,1523],{"id":921,"depth":70,"text":922},{"id":1016,"depth":70,"text":1017},{"id":1197,"depth":70,"text":1198},{"id":1403,"depth":64,"text":1404},{"id":1463,"depth":64,"text":1464},"Set statement_cache_size=0 and prepared_statement_cache_size=0 in your engine's connect_args — this disables asyncpg's prepared statement cache and eliminates all PgBouncer transaction-mode incompatibility errors, as detailed in the dialect-specific gotchas and driver quirks guide.","md",{"date":1529},"2026-06-18","\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Fhandling-asyncpg-prepared-statement-errors-with-pgbouncer",{"title":5,"description":1526},"async-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Fhandling-asyncpg-prepared-statement-errors-with-pgbouncer\u002Findex","sbSpLS0ibUK82IvA7mjjk9hJy7EehtUaNdvev_ztf-0",1781810028983]