[{"data":1,"prerenderedAt":1525},["ShallowReactive",2],{"page-\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Fconfiguring-pool-pre-ping-to-handle-stale-connections\u002F":3},{"id":4,"title":5,"body":6,"description":1517,"extension":1518,"meta":1519,"navigation":85,"path":1521,"seo":1522,"stem":1523,"__hash__":1524},"content\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Fconfiguring-pool-pre-ping-to-handle-stale-connections\u002Findex.md","Configuring Pool Pre-Ping to Handle Stale Connections",{"type":7,"value":8,"toc":1501},"minimark",[9,13,40,45,247,258,265,269,274,281,318,321,325,339,368,371,375,378,394,405,413,419,423,433,518,531,534,601,604,608,742,746,750,753,1146,1166,1170,1173,1392,1398,1402,1421,1427,1438,1451,1455,1497],[10,11,5],"h1",{"id":12},"configuring-pool-pre-ping-to-handle-stale-connections",[14,15,16,17,21,22,25,26,29,30,33,34,39],"p",{},"Set ",[18,19,20],"code",{},"pool_pre_ping=True"," in ",[18,23,24],{},"create_async_engine()"," to automatically discard connections whose underlying TCP socket was closed by the database or a network device while the connection sat idle in the pool — this is the direct fix for ",[18,27,28],{},"OperationalError: server closed the connection unexpectedly"," and ",[18,31,32],{},"asyncpg.exceptions.ConnectionDoesNotExistError"," in the ",[35,36,38],"a",{"href":37},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002F","handling connection leaks and pool exhaustion"," family of problems.",[41,42,44],"h2",{"id":43},"quick-answer","Quick Answer",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-python shiki shiki-themes github-light github-dark","# Before — stale connections handed out after idle timeout, causing OperationalError\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@db.internal\u002Fapp\",\n    pool_size=10,\n    max_overflow=5,\n)\n\n# After — pool validates each connection before checkout; stale sockets discarded\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@db.internal\u002Fapp\",\n    pool_size=10,\n    max_overflow=5,\n    pool_pre_ping=True,       # send a lightweight probe before each checkout\n    pool_recycle=1800,        # also recycle connections after 30 minutes (complement)\n)\n","python","",[18,53,54,63,80,87,99,109,124,137,143,148,154,165,170,179,186,197,208,225,242],{"__ignoreMap":51},[55,56,59],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"sJ8bj","# Before — stale connections handed out after idle timeout, causing OperationalError\n",[55,64,66,70,74,77],{"class":57,"line":65},2,[55,67,69],{"class":68},"szBVR","from",[55,71,73],{"class":72},"sVt8B"," sqlalchemy.ext.asyncio ",[55,75,76],{"class":68},"import",[55,78,79],{"class":72}," create_async_engine\n",[55,81,83],{"class":57,"line":82},3,[55,84,86],{"emptyLinePlaceholder":85},true,"\n",[55,88,90,93,96],{"class":57,"line":89},4,[55,91,92],{"class":72},"engine ",[55,94,95],{"class":68},"=",[55,97,98],{"class":72}," create_async_engine(\n",[55,100,102,106],{"class":57,"line":101},5,[55,103,105],{"class":104},"sZZnC","    \"postgresql+asyncpg:\u002F\u002Fuser:pass@db.internal\u002Fapp\"",[55,107,108],{"class":72},",\n",[55,110,112,116,118,122],{"class":57,"line":111},6,[55,113,115],{"class":114},"s4XuR","    pool_size",[55,117,95],{"class":68},[55,119,121],{"class":120},"sj4cs","10",[55,123,108],{"class":72},[55,125,127,130,132,135],{"class":57,"line":126},7,[55,128,129],{"class":114},"    max_overflow",[55,131,95],{"class":68},[55,133,134],{"class":120},"5",[55,136,108],{"class":72},[55,138,140],{"class":57,"line":139},8,[55,141,142],{"class":72},")\n",[55,144,146],{"class":57,"line":145},9,[55,147,86],{"emptyLinePlaceholder":85},[55,149,151],{"class":57,"line":150},10,[55,152,153],{"class":61},"# After — pool validates each connection before checkout; stale sockets discarded\n",[55,155,157,159,161,163],{"class":57,"line":156},11,[55,158,69],{"class":68},[55,160,73],{"class":72},[55,162,76],{"class":68},[55,164,79],{"class":72},[55,166,168],{"class":57,"line":167},12,[55,169,86],{"emptyLinePlaceholder":85},[55,171,173,175,177],{"class":57,"line":172},13,[55,174,92],{"class":72},[55,176,95],{"class":68},[55,178,98],{"class":72},[55,180,182,184],{"class":57,"line":181},14,[55,183,105],{"class":104},[55,185,108],{"class":72},[55,187,189,191,193,195],{"class":57,"line":188},15,[55,190,115],{"class":114},[55,192,95],{"class":68},[55,194,121],{"class":120},[55,196,108],{"class":72},[55,198,200,202,204,206],{"class":57,"line":199},16,[55,201,129],{"class":114},[55,203,95],{"class":68},[55,205,134],{"class":120},[55,207,108],{"class":72},[55,209,211,214,216,219,222],{"class":57,"line":210},17,[55,212,213],{"class":114},"    pool_pre_ping",[55,215,95],{"class":68},[55,217,218],{"class":120},"True",[55,220,221],{"class":72},",       ",[55,223,224],{"class":61},"# send a lightweight probe before each checkout\n",[55,226,228,231,233,236,239],{"class":57,"line":227},18,[55,229,230],{"class":114},"    pool_recycle",[55,232,95],{"class":68},[55,234,235],{"class":120},"1800",[55,237,238],{"class":72},",        ",[55,240,241],{"class":61},"# also recycle connections after 30 minutes (complement)\n",[55,243,245],{"class":57,"line":244},19,[55,246,142],{"class":72},[14,248,249,250,253,254,257],{},"Both parameters address the same root failure — a connection whose socket is no longer alive — from different angles. ",[18,251,252],{},"pool_pre_ping"," detects the problem at checkout time. ",[18,255,256],{},"pool_recycle"," prevents long-lived connections from ever reaching the age where cloud infrastructure is likely to terminate them.",[14,259,260,261,264],{},"The error they prevent is easy to reproduce: start your application, let the connection pool fill with established connections, wait longer than the database's idle connection timeout (or trigger a failover), then make a new request. Without pre-ping or recycle, the first few requests receive dead sockets and raise ",[18,262,263],{},"OperationalError",". With pre-ping, those dead connections are transparently replaced with fresh ones before your application code ever sees them.",[41,266,268],{"id":267},"execution-context-async-workflow-integration","Execution Context & Async Workflow Integration",[270,271,273],"h3",{"id":272},"why-connections-go-stale","Why Connections Go Stale",[14,275,276,277,280],{},"When a connection is checked back into ",[18,278,279],{},"AsyncAdaptedQueuePool",", it is kept open and reused for future checkouts. The connection's underlying TCP socket remains open to the database server. Several infrastructure events can close that socket while the connection appears healthy inside the pool:",[282,283,284,296,302,308],"ul",{},[285,286,287,291,292,295],"li",{},[288,289,290],"strong",{},"Cloud database idle connection timeouts"," — AWS RDS Postgres defaults to terminating connections idle for 8 hours via ",[18,293,294],{},"tcp_keepalives_idle",". Aurora and Cloud SQL have similar timeouts, some as short as 10–15 minutes for serverless tiers.",[285,297,298,301],{},[288,299,300],{},"Failover and replica promotion"," — during an RDS Multi-AZ failover, the old primary's connections are terminated. The pool holds references to sockets that now point at a dead endpoint.",[285,303,304,307],{},[288,305,306],{},"NAT gateway and load balancer timeouts"," — AWS NLBs time out idle TCP flows after 350 seconds by default. Connections sitting in the pool longer than this threshold become half-open sockets that appear active to the application but are dropped by the intermediary.",[285,309,310,317],{},[288,311,312,313,316],{},"Database ",[18,314,315],{},"pg_terminate_backend()"," calls"," — manual DBA intervention or automated maintenance scripts may terminate long-lived idle connections.",[14,319,320],{},"In all these cases, SQLAlchemy's pool has no way to know the socket is dead until it tries to use the connection. Without pre-ping, the first query on the stale connection raises an error.",[270,322,324],{"id":323},"what-pool_pre_ping-does","What pool_pre_ping Does",[14,326,327,328,330,331,334,335,338],{},"When ",[18,329,20],{}," is set, SQLAlchemy executes a dialect-appropriate probe query immediately before handing a connection to the caller. For ",[18,332,333],{},"asyncpg",", this is a lightweight ",[18,336,337],{},"SELECT 1"," executed over the existing socket. The sequence:",[340,341,342,353,356,362,365],"ol",{},[285,343,344,345,348,349,352],{},"Coroutine requests a connection via ",[18,346,347],{},"async with engine.connect()"," or ",[18,350,351],{},"async with AsyncSessionLocal() as session",".",[285,354,355],{},"Pool selects the least-recently-used connection from its queue.",[285,357,358,359,361],{},"Pool sends ",[18,360,337],{}," to the database over that connection's socket.",[285,363,364],{},"If the probe succeeds: connection is handed to the caller normally.",[285,366,367],{},"If the probe fails with a disconnect error: the connection is discarded, a fresh connection is opened, and the new connection is handed to the caller.",[14,369,370],{},"The caller never sees the stale connection. From the coroutine's perspective, checkout simply takes slightly longer than usual when a stale connection is encountered and replaced.",[270,372,374],{"id":373},"latency-cost-and-event-loop-impact","Latency Cost and Event Loop Impact",[14,376,377],{},"The probe adds one network round-trip per checkout for every connection that has been idle. On a LAN or VPC with 0.5–2 ms RTT to the database, this cost is negligible for most workloads. On high-throughput services making thousands of requests per second, the aggregate overhead becomes measurable.",[14,379,380,381,383,384,387,388,390,391,393],{},"Profile your actual p99 checkout latency with and without ",[18,382,252],{}," enabled using the pool event listeners described in the ",[35,385,386],{"href":37},"connection leaks and pool exhaustion guide",". If pre-ping adds more than 10% to your checkout overhead, use ",[18,389,256],{}," as the primary defence and reserve ",[18,392,252],{}," for environments with unpredictable disconnects (multi-AZ failovers, spot instance databases).",[14,395,396,397,400,401,404],{},"You can observe when pre-ping triggers by watching the ",[18,398,399],{},"sqlalchemy.pool"," log output with ",[18,402,403],{},"echo_pool=True",". A successful pre-ping produces no visible output — only a failed pre-ping (one that discards a stale connection) produces a log line similar to:",[46,406,411],{"className":407,"code":409,"language":410},[408],"language-text","Pool pre-ping on connection \u003Casyncpg...> failed, will attempt to reconnect (...)\n","text",[18,412,409],{"__ignoreMap":51},[14,414,415,416,418],{},"Seeing this message frequently is a signal that your ",[18,417,256],{}," setting is too long relative to the infrastructure's idle timeout, or that your database is restarting\u002Ffailing over frequently.",[270,420,422],{"id":421},"pool_recycle-as-a-complement","pool_recycle as a Complement",[14,424,425,428,429,432],{},[18,426,427],{},"pool_recycle=N"," tells the pool to discard any connection that has been open for more than ",[18,430,431],{},"N"," seconds, regardless of activity. It prevents connections from living long enough for infrastructure timeouts to close them in the first place:",[46,434,436],{"className":48,"code":435,"language":50,"meta":51,"style":51},"from sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@db.internal\u002Fapp\",\n    pool_size=10,\n    max_overflow=5,\n    pool_recycle=1800,   # recycle before RDS's 8-hour idle timeout\n    pool_pre_ping=True,  # catch any that slip through during failover\n)\n",[18,437,438,448,452,460,466,476,486,500,514],{"__ignoreMap":51},[55,439,440,442,444,446],{"class":57,"line":58},[55,441,69],{"class":68},[55,443,73],{"class":72},[55,445,76],{"class":68},[55,447,79],{"class":72},[55,449,450],{"class":57,"line":65},[55,451,86],{"emptyLinePlaceholder":85},[55,453,454,456,458],{"class":57,"line":82},[55,455,92],{"class":72},[55,457,95],{"class":68},[55,459,98],{"class":72},[55,461,462,464],{"class":57,"line":89},[55,463,105],{"class":104},[55,465,108],{"class":72},[55,467,468,470,472,474],{"class":57,"line":101},[55,469,115],{"class":114},[55,471,95],{"class":68},[55,473,121],{"class":120},[55,475,108],{"class":72},[55,477,478,480,482,484],{"class":57,"line":111},[55,479,129],{"class":114},[55,481,95],{"class":68},[55,483,134],{"class":120},[55,485,108],{"class":72},[55,487,488,490,492,494,497],{"class":57,"line":126},[55,489,230],{"class":114},[55,491,95],{"class":68},[55,493,235],{"class":120},[55,495,496],{"class":72},",   ",[55,498,499],{"class":61},"# recycle before RDS's 8-hour idle timeout\n",[55,501,502,504,506,508,511],{"class":57,"line":139},[55,503,213],{"class":114},[55,505,95],{"class":68},[55,507,218],{"class":120},[55,509,510],{"class":72},",  ",[55,512,513],{"class":61},"# catch any that slip through during failover\n",[55,515,516],{"class":57,"line":145},[55,517,142],{"class":72},[14,519,520,521,523,524,527,528,352],{},"A practical rule: set ",[18,522,256],{}," to less than half the database's idle connection timeout. For RDS Postgres (default 8 hours), ",[18,525,526],{},"pool_recycle=1800"," (30 minutes) is conservative. For Aurora Serverless with a 10-minute idle pause timeout, use ",[18,529,530],{},"pool_recycle=300",[14,532,533],{},"The two parameters are not redundant — they protect against different failure modes:",[535,536,537,553],"table",{},[538,539,540],"thead",{},[541,542,543,547,550],"tr",{},[544,545,546],"th",{},"Scenario",[544,548,549],{},"pool_pre_ping protects you",[544,551,552],{},"pool_recycle protects you",[554,555,556,568,579,590],"tbody",{},[541,557,558,562,565],{},[559,560,561],"td",{},"Database idle connection timeout (predictable)",[559,563,564],{},"Partially — catches at next checkout",[559,566,567],{},"Yes — connection never reaches the age threshold",[541,569,570,573,576],{},[559,571,572],{},"Multi-AZ failover (sudden)",[559,574,575],{},"Yes — detects dead socket at checkout",[559,577,578],{},"No — recycle doesn't help if failover happens between recycle and next checkout",[541,580,581,584,587],{},[559,582,583],{},"NAT gateway TCP idle timeout (unpredictable)",[559,585,586],{},"Yes",[559,588,589],{},"Only if recycle interval is shorter than NAT timeout",[541,591,592,595,598],{},[559,593,594],{},"High-latency environment (p99 checkout > 5ms SLA)",[559,596,597],{},"Adds latency — consider disabling",[559,599,600],{},"No latency at checkout",[14,602,603],{},"For cloud PostgreSQL on RDS, Aurora, or Cloud SQL, both parameters together form the minimum viable stale-connection defence.",[41,605,607],{"id":606},"resolving-warnings-errors-common-mistakes","Resolving Warnings, Errors & Common Mistakes",[535,609,610,623],{},[538,611,612],{},[541,613,614,617,620],{},[544,615,616],{},"Error \u002F Warning",[544,618,619],{},"Root Cause",[544,621,622],{},"Production Fix",[554,624,625,643,658,678,691,710,728],{},[541,626,627,631,634],{},[559,628,629],{},[18,630,28],{},[559,632,633],{},"TCP socket closed by DB\u002Fnetwork while connection was idle in pool",[559,635,636,637,639,640,642],{},"Add ",[18,638,20],{},"; set ",[18,641,256],{}," below DB idle timeout",[541,644,645,649,652],{},[559,646,647],{},[18,648,32],{},[559,650,651],{},"asyncpg attempted to use a connection whose socket is gone",[559,653,654,655,657],{},"Same — ",[18,656,20],{}," catches the stale connection before asyncpg touches it",[541,659,660,665,668],{},[559,661,662],{},[18,663,664],{},"asyncpg.exceptions.InterfaceError: connection is closed",[559,666,667],{},"Connection already invalidated (often by a prior failed transaction)",[559,669,670,671,674,675,677],{},"Ensure ",[18,672,673],{},"async with session.begin():"," is used; add ",[18,676,20],{}," for robustness",[541,679,680,685,688],{},[559,681,682],{},[18,683,684],{},"sqlalchemy.exc.TimeoutError: QueuePool limit of size X overflow Y reached",[559,686,687],{},"Connections not returned to pool (leak), unrelated to pre-ping",[559,689,690],{},"Fix context-manager discipline; pre-ping does NOT help with leaks",[541,692,693,698,701],{},[559,694,695],{},[18,696,697],{},"OperationalError: SSL SYSCALL error: EOF detected",[559,699,700],{},"SSL connection terminated mid-flight (often during failover)",[559,702,636,703,705,706,709],{},[18,704,20],{},"; also configure asyncpg ",[18,707,708],{},"ssl='require'"," with reconnect logic",[541,711,712,715,718],{},[559,713,714],{},"High checkout latency (p99 spikes)",[559,716,717],{},"Pre-ping probe on every checkout under high throughput",[559,719,720,721,723,724,727],{},"Use ",[18,722,256],{}," instead of pre-ping for latency-sensitive paths; or tune ",[18,725,726],{},"pool_size"," up to reduce stale-connection frequency",[541,729,730,736,739],{},[559,731,732,735],{},[18,733,734],{},"WARNING: pool pre-ping failed, reconnecting"," in logs",[559,737,738],{},"Pre-ping detected a dead connection; a new connection is being made",[559,740,741],{},"This is the intended behaviour — only a concern if it fires very frequently (investigate DB restarts or NAT timeout)",[41,743,745],{"id":744},"advanced-pool-pre-ping-optimization","Advanced Pool Pre-Ping Optimization",[270,747,749],{"id":748},"selective-pre-ping-with-connection-age-tracking","Selective Pre-Ping with Connection Age Tracking",[14,751,752],{},"If pre-ping overhead is measurable but you still need stale-connection protection, you can apply the probe selectively: only check connections that have been idle longer than a threshold. This requires wrapping the checkout event to track last-use time:",[46,754,756],{"className":48,"code":755,"language":50,"meta":51,"style":51},"import time\nfrom sqlalchemy import event\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine\n\n# Build engine WITHOUT global pre-ping — we will implement selective checking\nengine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fuser:pass@db.internal\u002Fapp\",\n    pool_size=10,\n    max_overflow=5,\n    pool_recycle=1800,\n    pool_pre_ping=False,  # disabled globally\n)\n\nSTALE_THRESHOLD_SECONDS = 60  # probe connections idle for more than 60s\n\n\ndef attach_selective_pre_ping(eng: AsyncEngine) -> None:\n    sync_pool = eng.sync_engine.pool\n    last_checkin: dict[int, float] = {}\n\n    @event.listens_for(sync_pool, \"checkin\")\n    def record_checkin(dbapi_conn, conn_record) -> None:\n        last_checkin[id(dbapi_conn)] = time.monotonic()\n\n    @event.listens_for(sync_pool, \"checkout\")\n    def selective_ping(dbapi_conn, conn_record, conn_proxy) -> None:\n        conn_id = id(dbapi_conn)\n        idle_for = time.monotonic() - last_checkin.get(conn_id, 0)\n        if idle_for > STALE_THRESHOLD_SECONDS:\n            # Manually test the connection using the dialect's ping mechanism\n            try:\n                dbapi_conn.ping(reconnect=False)\n            except Exception:\n                # invalidate forces the pool to open a fresh connection\n                conn_record.invalidate()\n                raise\n\n\nattach_selective_pre_ping(engine)\n",[18,757,758,765,777,788,792,797,805,811,821,831,841,855,859,863,877,881,885,903,913,935,940,954,970,987,992,1004,1019,1033,1055,1072,1078,1086,1101,1112,1118,1124,1130,1135,1140],{"__ignoreMap":51},[55,759,760,762],{"class":57,"line":58},[55,761,76],{"class":68},[55,763,764],{"class":72}," time\n",[55,766,767,769,772,774],{"class":57,"line":65},[55,768,69],{"class":68},[55,770,771],{"class":72}," sqlalchemy ",[55,773,76],{"class":68},[55,775,776],{"class":72}," event\n",[55,778,779,781,783,785],{"class":57,"line":82},[55,780,69],{"class":68},[55,782,73],{"class":72},[55,784,76],{"class":68},[55,786,787],{"class":72}," create_async_engine, AsyncEngine\n",[55,789,790],{"class":57,"line":89},[55,791,86],{"emptyLinePlaceholder":85},[55,793,794],{"class":57,"line":101},[55,795,796],{"class":61},"# Build engine WITHOUT global pre-ping — we will implement selective checking\n",[55,798,799,801,803],{"class":57,"line":111},[55,800,92],{"class":72},[55,802,95],{"class":68},[55,804,98],{"class":72},[55,806,807,809],{"class":57,"line":126},[55,808,105],{"class":104},[55,810,108],{"class":72},[55,812,813,815,817,819],{"class":57,"line":139},[55,814,115],{"class":114},[55,816,95],{"class":68},[55,818,121],{"class":120},[55,820,108],{"class":72},[55,822,823,825,827,829],{"class":57,"line":145},[55,824,129],{"class":114},[55,826,95],{"class":68},[55,828,134],{"class":120},[55,830,108],{"class":72},[55,832,833,835,837,839],{"class":57,"line":150},[55,834,230],{"class":114},[55,836,95],{"class":68},[55,838,235],{"class":120},[55,840,108],{"class":72},[55,842,843,845,847,850,852],{"class":57,"line":156},[55,844,213],{"class":114},[55,846,95],{"class":68},[55,848,849],{"class":120},"False",[55,851,510],{"class":72},[55,853,854],{"class":61},"# disabled globally\n",[55,856,857],{"class":57,"line":167},[55,858,142],{"class":72},[55,860,861],{"class":57,"line":172},[55,862,86],{"emptyLinePlaceholder":85},[55,864,865,868,871,874],{"class":57,"line":181},[55,866,867],{"class":120},"STALE_THRESHOLD_SECONDS",[55,869,870],{"class":68}," =",[55,872,873],{"class":120}," 60",[55,875,876],{"class":61},"  # probe connections idle for more than 60s\n",[55,878,879],{"class":57,"line":188},[55,880,86],{"emptyLinePlaceholder":85},[55,882,883],{"class":57,"line":199},[55,884,86],{"emptyLinePlaceholder":85},[55,886,887,890,894,897,900],{"class":57,"line":210},[55,888,889],{"class":68},"def",[55,891,893],{"class":892},"sScJk"," attach_selective_pre_ping",[55,895,896],{"class":72},"(eng: AsyncEngine) -> ",[55,898,899],{"class":120},"None",[55,901,902],{"class":72},":\n",[55,904,905,908,910],{"class":57,"line":227},[55,906,907],{"class":72},"    sync_pool ",[55,909,95],{"class":68},[55,911,912],{"class":72}," eng.sync_engine.pool\n",[55,914,915,918,921,924,927,930,932],{"class":57,"line":244},[55,916,917],{"class":72},"    last_checkin: dict[",[55,919,920],{"class":120},"int",[55,922,923],{"class":72},", ",[55,925,926],{"class":120},"float",[55,928,929],{"class":72},"] ",[55,931,95],{"class":68},[55,933,934],{"class":72}," {}\n",[55,936,938],{"class":57,"line":937},20,[55,939,86],{"emptyLinePlaceholder":85},[55,941,943,946,949,952],{"class":57,"line":942},21,[55,944,945],{"class":892},"    @event.listens_for",[55,947,948],{"class":72},"(sync_pool, ",[55,950,951],{"class":104},"\"checkin\"",[55,953,142],{"class":72},[55,955,957,960,963,966,968],{"class":57,"line":956},22,[55,958,959],{"class":68},"    def",[55,961,962],{"class":892}," record_checkin",[55,964,965],{"class":72},"(dbapi_conn, conn_record) -> ",[55,967,899],{"class":120},[55,969,902],{"class":72},[55,971,973,976,979,982,984],{"class":57,"line":972},23,[55,974,975],{"class":72},"        last_checkin[",[55,977,978],{"class":120},"id",[55,980,981],{"class":72},"(dbapi_conn)] ",[55,983,95],{"class":68},[55,985,986],{"class":72}," time.monotonic()\n",[55,988,990],{"class":57,"line":989},24,[55,991,86],{"emptyLinePlaceholder":85},[55,993,995,997,999,1002],{"class":57,"line":994},25,[55,996,945],{"class":892},[55,998,948],{"class":72},[55,1000,1001],{"class":104},"\"checkout\"",[55,1003,142],{"class":72},[55,1005,1007,1009,1012,1015,1017],{"class":57,"line":1006},26,[55,1008,959],{"class":68},[55,1010,1011],{"class":892}," selective_ping",[55,1013,1014],{"class":72},"(dbapi_conn, conn_record, conn_proxy) -> ",[55,1016,899],{"class":120},[55,1018,902],{"class":72},[55,1020,1022,1025,1027,1030],{"class":57,"line":1021},27,[55,1023,1024],{"class":72},"        conn_id ",[55,1026,95],{"class":68},[55,1028,1029],{"class":120}," id",[55,1031,1032],{"class":72},"(dbapi_conn)\n",[55,1034,1036,1039,1041,1044,1047,1050,1053],{"class":57,"line":1035},28,[55,1037,1038],{"class":72},"        idle_for ",[55,1040,95],{"class":68},[55,1042,1043],{"class":72}," time.monotonic() ",[55,1045,1046],{"class":68},"-",[55,1048,1049],{"class":72}," last_checkin.get(conn_id, ",[55,1051,1052],{"class":120},"0",[55,1054,142],{"class":72},[55,1056,1058,1061,1064,1067,1070],{"class":57,"line":1057},29,[55,1059,1060],{"class":68},"        if",[55,1062,1063],{"class":72}," idle_for ",[55,1065,1066],{"class":68},">",[55,1068,1069],{"class":120}," STALE_THRESHOLD_SECONDS",[55,1071,902],{"class":72},[55,1073,1075],{"class":57,"line":1074},30,[55,1076,1077],{"class":61},"            # Manually test the connection using the dialect's ping mechanism\n",[55,1079,1081,1084],{"class":57,"line":1080},31,[55,1082,1083],{"class":68},"            try",[55,1085,902],{"class":72},[55,1087,1089,1092,1095,1097,1099],{"class":57,"line":1088},32,[55,1090,1091],{"class":72},"                dbapi_conn.ping(",[55,1093,1094],{"class":114},"reconnect",[55,1096,95],{"class":68},[55,1098,849],{"class":120},[55,1100,142],{"class":72},[55,1102,1104,1107,1110],{"class":57,"line":1103},33,[55,1105,1106],{"class":68},"            except",[55,1108,1109],{"class":120}," Exception",[55,1111,902],{"class":72},[55,1113,1115],{"class":57,"line":1114},34,[55,1116,1117],{"class":61},"                # invalidate forces the pool to open a fresh connection\n",[55,1119,1121],{"class":57,"line":1120},35,[55,1122,1123],{"class":72},"                conn_record.invalidate()\n",[55,1125,1127],{"class":57,"line":1126},36,[55,1128,1129],{"class":68},"                raise\n",[55,1131,1133],{"class":57,"line":1132},37,[55,1134,86],{"emptyLinePlaceholder":85},[55,1136,1138],{"class":57,"line":1137},38,[55,1139,86],{"emptyLinePlaceholder":85},[55,1141,1143],{"class":57,"line":1142},39,[55,1144,1145],{"class":72},"attach_selective_pre_ping(engine)\n",[14,1147,1148,1149,1152,1153,1156,1157,1159,1160,1163,1164,352],{},"Note: ",[18,1150,1151],{},"dbapi_conn.ping()"," is available on ",[18,1154,1155],{},"aiomysql"," connections. For ",[18,1158,333],{},", the pool-level invalidation on ",[18,1161,1162],{},"checkout"," failure is handled by SQLAlchemy's pre-ping machinery itself — the most reliable approach for asyncpg remains the built-in ",[18,1165,20],{},[270,1167,1169],{"id":1168},"combining-pool_pre_ping-with-retry-logic","Combining pool_pre_ping with Retry Logic",[14,1171,1172],{},"Pre-ping handles the idle-pool case, but a stale connection can also appear mid-transaction during a failover. For full resilience, add application-level retry around the operation:",[46,1174,1176],{"className":48,"code":1175,"language":50,"meta":51,"style":51},"import asyncio\nfrom sqlalchemy.exc import OperationalError\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nasync def execute_with_retry(session: AsyncSession, stmt, *, retries: int = 2):\n    \"\"\"Execute a statement, retrying once on disconnect errors.\"\"\"\n    for attempt in range(retries + 1):\n        try:\n            result = await session.execute(stmt)\n            return result\n        except OperationalError as exc:\n            if attempt \u003C retries and \"server closed the connection\" in str(exc):\n                await asyncio.sleep(0.1 * (2 ** attempt))  # brief backoff\n                await session.rollback()\n                continue\n            raise\n",[18,1177,1178,1185,1197,1208,1212,1216,1246,1251,1276,1283,1296,1304,1318,1346,1375,1382,1387],{"__ignoreMap":51},[55,1179,1180,1182],{"class":57,"line":58},[55,1181,76],{"class":68},[55,1183,1184],{"class":72}," asyncio\n",[55,1186,1187,1189,1192,1194],{"class":57,"line":65},[55,1188,69],{"class":68},[55,1190,1191],{"class":72}," sqlalchemy.exc ",[55,1193,76],{"class":68},[55,1195,1196],{"class":72}," OperationalError\n",[55,1198,1199,1201,1203,1205],{"class":57,"line":82},[55,1200,69],{"class":68},[55,1202,73],{"class":72},[55,1204,76],{"class":68},[55,1206,1207],{"class":72}," AsyncSession\n",[55,1209,1210],{"class":57,"line":89},[55,1211,86],{"emptyLinePlaceholder":85},[55,1213,1214],{"class":57,"line":101},[55,1215,86],{"emptyLinePlaceholder":85},[55,1217,1218,1221,1224,1227,1230,1233,1236,1238,1240,1243],{"class":57,"line":111},[55,1219,1220],{"class":68},"async",[55,1222,1223],{"class":68}," def",[55,1225,1226],{"class":892}," execute_with_retry",[55,1228,1229],{"class":72},"(session: AsyncSession, stmt, ",[55,1231,1232],{"class":68},"*",[55,1234,1235],{"class":72},", retries: ",[55,1237,920],{"class":120},[55,1239,870],{"class":68},[55,1241,1242],{"class":120}," 2",[55,1244,1245],{"class":72},"):\n",[55,1247,1248],{"class":57,"line":126},[55,1249,1250],{"class":104},"    \"\"\"Execute a statement, retrying once on disconnect errors.\"\"\"\n",[55,1252,1253,1256,1259,1262,1265,1268,1271,1274],{"class":57,"line":139},[55,1254,1255],{"class":68},"    for",[55,1257,1258],{"class":72}," attempt ",[55,1260,1261],{"class":68},"in",[55,1263,1264],{"class":120}," range",[55,1266,1267],{"class":72},"(retries ",[55,1269,1270],{"class":68},"+",[55,1272,1273],{"class":120}," 1",[55,1275,1245],{"class":72},[55,1277,1278,1281],{"class":57,"line":145},[55,1279,1280],{"class":68},"        try",[55,1282,902],{"class":72},[55,1284,1285,1288,1290,1293],{"class":57,"line":150},[55,1286,1287],{"class":72},"            result ",[55,1289,95],{"class":68},[55,1291,1292],{"class":68}," await",[55,1294,1295],{"class":72}," session.execute(stmt)\n",[55,1297,1298,1301],{"class":57,"line":156},[55,1299,1300],{"class":68},"            return",[55,1302,1303],{"class":72}," result\n",[55,1305,1306,1309,1312,1315],{"class":57,"line":167},[55,1307,1308],{"class":68},"        except",[55,1310,1311],{"class":72}," OperationalError ",[55,1313,1314],{"class":68},"as",[55,1316,1317],{"class":72}," exc:\n",[55,1319,1320,1323,1325,1328,1331,1334,1337,1340,1343],{"class":57,"line":172},[55,1321,1322],{"class":68},"            if",[55,1324,1258],{"class":72},[55,1326,1327],{"class":68},"\u003C",[55,1329,1330],{"class":72}," retries ",[55,1332,1333],{"class":68},"and",[55,1335,1336],{"class":104}," \"server closed the connection\"",[55,1338,1339],{"class":68}," in",[55,1341,1342],{"class":120}," str",[55,1344,1345],{"class":72},"(exc):\n",[55,1347,1348,1351,1354,1357,1360,1363,1366,1369,1372],{"class":57,"line":181},[55,1349,1350],{"class":68},"                await",[55,1352,1353],{"class":72}," asyncio.sleep(",[55,1355,1356],{"class":120},"0.1",[55,1358,1359],{"class":68}," *",[55,1361,1362],{"class":72}," (",[55,1364,1365],{"class":120},"2",[55,1367,1368],{"class":68}," **",[55,1370,1371],{"class":72}," attempt))  ",[55,1373,1374],{"class":61},"# brief backoff\n",[55,1376,1377,1379],{"class":57,"line":188},[55,1378,1350],{"class":68},[55,1380,1381],{"class":72}," session.rollback()\n",[55,1383,1384],{"class":57,"line":199},[55,1385,1386],{"class":68},"                continue\n",[55,1388,1389],{"class":57,"line":210},[55,1390,1391],{"class":68},"            raise\n",[14,1393,1394,1395,1397],{},"This retry wrapper should be narrow — only catching ",[18,1396,263],{}," with known disconnect messages — to avoid masking genuine query errors.",[41,1399,1401],{"id":1400},"frequently-asked-questions","Frequently Asked Questions",[14,1403,1404,1407,1408,1411,1412,1414,1415,29,1417,1420],{},[288,1405,1406],{},"Does pool_pre_ping work with asyncpg specifically?","\nYes. For the ",[18,1409,1410],{},"postgresql+asyncpg"," dialect, SQLAlchemy's pre-ping sends ",[18,1413,337],{}," through asyncpg's connection and catches ",[18,1416,32],{},[18,1418,1419],{},"asyncpg.exceptions.InterfaceError"," as indicators of a dead connection. The stale connection is invalidated and a fresh one is transparently substituted before the connection reaches your code.",[14,1422,1423,1426],{},[288,1424,1425],{},"Should I use pool_pre_ping=True for every async engine?","\nUse it whenever: your application is deployed against a cloud-managed database (RDS, Cloud SQL, Azure Database), your traffic has long idle periods between requests (the pool holds connections but no queries run for minutes), or you operate in a multi-AZ or replica-set topology where failovers terminate connections. Skip it (or combine with selective logic) if you have a latency SLA under 5 ms and your pool is never idle long enough for sockets to go stale.",[14,1428,1429,1432,1434,1435,1437],{},[288,1430,1431],{},"What is the difference between pool_pre_ping and pool_recycle?",[18,1433,252],{}," probes the connection at checkout time — reactive, adds latency, catches any disconnect regardless of cause. ",[18,1436,256],{}," proactively discards connections older than N seconds before they can go stale — preventive, zero latency at checkout, but does not protect against unexpected disconnects like failovers. Use both together for the strongest protection.",[14,1439,1440,1443,1444,1447,1448,1450],{},[288,1441,1442],{},"Will pool_pre_ping prevent the QueuePool limit error?","\nNo. Pool exhaustion (",[18,1445,1446],{},"QueuePool limit of size X overflow Y reached",") is caused by connections being checked out and never returned — a leak. Pre-ping only acts on connections that are already back in the pool (idle, checked in). If your pool is exhausted, the problem is context-manager discipline or long-running transactions, not stale sockets. See the ",[35,1449,386],{"href":37}," for the correct diagnosis.",[41,1452,1454],{"id":1453},"related","Related",[282,1456,1457,1467,1487],{},[285,1458,1459,1462,1463,1466],{},[35,1460,1461],{"href":37},"Handling Connection Leaks and Pool Exhaustion"," — parent guide covering the full spectrum of pool problems including the ",[18,1464,1465],{},"QueuePool limit"," error and context-manager discipline.",[285,1468,1469,1473,1474,923,1476,923,1479,1482,1483,1486],{},[35,1470,1472],{"href":1471},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools"," — complete reference for ",[18,1475,726],{},[18,1477,1478],{},"max_overflow",[18,1480,1481],{},"pool_timeout",", and ",[18,1484,1485],{},"NullPool"," configuration.",[285,1488,1489,1493,1494,1496],{},[35,1490,1492],{"href":1491},"\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002F","Tuning Connection Pools for Cloud Databases"," — RDS, Aurora, and Cloud SQL specific idle-timeout values and recommended ",[18,1495,256],{}," settings.",[1498,1499,1500],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":51,"searchDepth":65,"depth":65,"links":1502},[1503,1504,1510,1511,1515,1516],{"id":43,"depth":65,"text":44},{"id":267,"depth":65,"text":268,"children":1505},[1506,1507,1508,1509],{"id":272,"depth":82,"text":273},{"id":323,"depth":82,"text":324},{"id":373,"depth":82,"text":374},{"id":421,"depth":82,"text":422},{"id":606,"depth":65,"text":607},{"id":744,"depth":65,"text":745,"children":1512},[1513,1514],{"id":748,"depth":82,"text":749},{"id":1168,"depth":82,"text":1169},{"id":1400,"depth":65,"text":1401},{"id":1453,"depth":65,"text":1454},"Set pool_pre_ping=True in create_async_engine() to automatically discard connections whose underlying TCP socket was closed by the database or a network device while the connection sat idle in the pool — this is the direct fix for OperationalError: server closed the connection unexpectedly and asyncpg.exceptions.ConnectionDoesNotExistError in the handling connection leaks and pool exhaustion family of problems.","md",{"date":1520},"2026-06-18","\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Fconfiguring-pool-pre-ping-to-handle-stale-connections",{"title":5,"description":1517},"async-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Fconfiguring-pool-pre-ping-to-handle-stale-connections\u002Findex","tPUd-KxeP0hmtAjES6sarge3PBlekck571boyGXzd7g",1781810028983]