[{"data":1,"prerenderedAt":1543},["ShallowReactive",2],{"page-\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002F":3},{"id":4,"title":5,"body":6,"description":1536,"extension":1537,"meta":1538,"navigation":97,"path":1539,"seo":1540,"stem":1541,"__hash__":1542},"content\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Findex.md","Handling Connection Leaks and Pool Exhaustion",{"type":7,"value":8,"toc":1527},"minimark",[9,13,34,47,52,63,70,139,166,187,393,397,422,429,436,713,717,728,731,753,871,875,881,884,887,1030,1034,1044,1047,1054,1376,1380,1459,1463,1481,1487,1502,1523],[10,11,5],"h1",{"id":12},"handling-connection-leaks-and-pool-exhaustion",[14,15,16,17,21,22,25,26,29,30,33],"p",{},"In asynchronous Python applications, the database connection lifecycle is strictly bound to the event loop rather than thread-local state. Unlike synchronous SQLAlchemy, where deterministic cleanup often relies on garbage collection or explicit ",[18,19,20],"code",{},"close()"," calls in ",[18,23,24],{},"finally"," blocks, async execution introduces critical boundaries: unawaited coroutines, missing ",[18,27,28],{},"async with"," context managers, and unhandled exceptions can silently retain connections, triggering pool starvation. Understanding these async\u002Fawait boundaries is foundational to preventing ",[18,31,32],{},"PoolTimeout"," errors in production.",[14,35,36,37,40,41,46],{},"The architecture of SQLAlchemy 2.0's async pooling relies on ",[18,38,39],{},"asyncio.Queue"," under the hood, managing a fixed set of database connections that are checked out, utilized, and returned to the pool. When connections are not explicitly returned due to improper coroutine scheduling or exception propagation, the pool's available slots deplete. For a comprehensive overview of how async engines map to underlying connection managers, consult ",[42,43,45],"a",{"href":44},"\u002Fasync-engines-dialects-and-connection-pooling\u002F","Async Engines, Dialects, and Connection Pooling",". This article details diagnostic patterns, driver-specific mitigation, and production-ready remediation strategies for connection leaks and pool exhaustion.",[48,49,51],"h2",{"id":50},"diagnosing-pool-exhaustion-and-leak-vectors","Diagnosing Pool Exhaustion and Leak Vectors",[14,53,54,55,58,59,62],{},"Pool exhaustion typically manifests as ",[18,56,57],{},"sqlalchemy.exc.PoolTimeout",", raised when the checkout queue waits beyond ",[18,60,61],{},"pool_timeout"," (default: 30 seconds) without acquiring a connection. In production, this is rarely a sudden database failure; it is almost always a symptom of connection retention.",[14,64,65,66,69],{},"To diagnose leaks, enable structured logging for the ",[18,67,68],{},"sqlalchemy.pool"," namespace. Tracking checkout\u002Fcheckin deltas reveals connections that enter the pool but never return:",[71,72,77],"pre",{"className":73,"code":74,"language":75,"meta":76,"style":76},"language-python shiki shiki-themes github-light github-dark","import logging\n\nlogging.basicConfig(level=logging.DEBUG)\nlogging.getLogger(\"sqlalchemy.pool\").setLevel(logging.DEBUG)\n","python","",[18,78,79,92,99,122],{"__ignoreMap":76},[80,81,84,88],"span",{"class":82,"line":83},"line",1,[80,85,87],{"class":86},"szBVR","import",[80,89,91],{"class":90},"sVt8B"," logging\n",[80,93,95],{"class":82,"line":94},2,[80,96,98],{"emptyLinePlaceholder":97},true,"\n",[80,100,102,105,109,112,115,119],{"class":82,"line":101},3,[80,103,104],{"class":90},"logging.basicConfig(",[80,106,108],{"class":107},"s4XuR","level",[80,110,111],{"class":86},"=",[80,113,114],{"class":90},"logging.",[80,116,118],{"class":117},"sj4cs","DEBUG",[80,120,121],{"class":90},")\n",[80,123,125,128,132,135,137],{"class":82,"line":124},4,[80,126,127],{"class":90},"logging.getLogger(",[80,129,131],{"class":130},"sZZnC","\"sqlalchemy.pool\"",[80,133,134],{"class":90},").setLevel(logging.",[80,136,118],{"class":117},[80,138,121],{"class":90},[14,140,141,142,145,146,149,150,153,154,157,158,160,161,165],{},"Look for ",[18,143,144],{},"Connection \u003C...> checked out"," without a corresponding ",[18,147,148],{},"checked in",". Baseline tuning parameters like ",[18,151,152],{},"pool_size",", ",[18,155,156],{},"max_overflow",", and ",[18,159,61],{}," must align with your application's concurrency model. For detailed guidance on mapping these parameters to workload characteristics, review ",[42,162,164],{"href":163},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools",".",[14,167,168,169,153,172,157,175,178,179,182,183,186],{},"Beyond logs, implement real-time telemetry using Prometheus or OpenTelemetry. Expose metrics such as ",[18,170,171],{},"pool.checkedout()",[18,173,174],{},"pool.overflow()",[18,176,177],{},"pool.size()"," via a dedicated ",[18,180,181],{},"\u002Fmetrics"," endpoint. Sudden spikes in ",[18,184,185],{},"checkedout"," without corresponding query throughput indicate a leak vector.",[71,188,190],{"className":73,"code":189,"language":75,"meta":76,"style":76},"from sqlalchemy import event\nfrom sqlalchemy.ext.asyncio import AsyncEngine\nfrom sqlalchemy.exc import PoolTimeout\nimport logging\n\nlogger = logging.getLogger(\"app.pool_monitor\")\n\n@event.listens_for(AsyncEngine, \"handle_error\")\ndef intercept_pool_errors(context: event.PoolEventContext) -> None:\n \"\"\"Intercept PoolTimeout and emit structured telemetry for alerting.\"\"\"\n if isinstance(context.exception, PoolTimeout):\n logger.error(\n \"PoolTimeout detected\",\n extra={\n \"pool_size\": context.pool.size(),\n \"checked_out\": context.pool.checkedout(),\n \"overflow\": context.pool.overflow(),\n \"timeout\": context.pool.timeout,\n },\n )\n # Allow default error handling to proceed\n",[18,191,192,205,217,229,235,240,256,261,276,294,300,312,318,327,338,347,356,365,374,380,386],{"__ignoreMap":76},[80,193,194,197,200,202],{"class":82,"line":83},[80,195,196],{"class":86},"from",[80,198,199],{"class":90}," sqlalchemy ",[80,201,87],{"class":86},[80,203,204],{"class":90}," event\n",[80,206,207,209,212,214],{"class":82,"line":94},[80,208,196],{"class":86},[80,210,211],{"class":90}," sqlalchemy.ext.asyncio ",[80,213,87],{"class":86},[80,215,216],{"class":90}," AsyncEngine\n",[80,218,219,221,224,226],{"class":82,"line":101},[80,220,196],{"class":86},[80,222,223],{"class":90}," sqlalchemy.exc ",[80,225,87],{"class":86},[80,227,228],{"class":90}," PoolTimeout\n",[80,230,231,233],{"class":82,"line":124},[80,232,87],{"class":86},[80,234,91],{"class":90},[80,236,238],{"class":82,"line":237},5,[80,239,98],{"emptyLinePlaceholder":97},[80,241,243,246,248,251,254],{"class":82,"line":242},6,[80,244,245],{"class":90},"logger ",[80,247,111],{"class":86},[80,249,250],{"class":90}," logging.getLogger(",[80,252,253],{"class":130},"\"app.pool_monitor\"",[80,255,121],{"class":90},[80,257,259],{"class":82,"line":258},7,[80,260,98],{"emptyLinePlaceholder":97},[80,262,264,268,271,274],{"class":82,"line":263},8,[80,265,267],{"class":266},"sScJk","@event.listens_for",[80,269,270],{"class":90},"(AsyncEngine, ",[80,272,273],{"class":130},"\"handle_error\"",[80,275,121],{"class":90},[80,277,279,282,285,288,291],{"class":82,"line":278},9,[80,280,281],{"class":86},"def",[80,283,284],{"class":266}," intercept_pool_errors",[80,286,287],{"class":90},"(context: event.PoolEventContext) -> ",[80,289,290],{"class":117},"None",[80,292,293],{"class":90},":\n",[80,295,297],{"class":82,"line":296},10,[80,298,299],{"class":130}," \"\"\"Intercept PoolTimeout and emit structured telemetry for alerting.\"\"\"\n",[80,301,303,306,309],{"class":82,"line":302},11,[80,304,305],{"class":86}," if",[80,307,308],{"class":117}," isinstance",[80,310,311],{"class":90},"(context.exception, PoolTimeout):\n",[80,313,315],{"class":82,"line":314},12,[80,316,317],{"class":90}," logger.error(\n",[80,319,321,324],{"class":82,"line":320},13,[80,322,323],{"class":130}," \"PoolTimeout detected\"",[80,325,326],{"class":90},",\n",[80,328,330,333,335],{"class":82,"line":329},14,[80,331,332],{"class":107}," extra",[80,334,111],{"class":86},[80,336,337],{"class":90},"{\n",[80,339,341,344],{"class":82,"line":340},15,[80,342,343],{"class":130}," \"pool_size\"",[80,345,346],{"class":90},": context.pool.size(),\n",[80,348,350,353],{"class":82,"line":349},16,[80,351,352],{"class":130}," \"checked_out\"",[80,354,355],{"class":90},": context.pool.checkedout(),\n",[80,357,359,362],{"class":82,"line":358},17,[80,360,361],{"class":130}," \"overflow\"",[80,363,364],{"class":90},": context.pool.overflow(),\n",[80,366,368,371],{"class":82,"line":367},18,[80,369,370],{"class":130}," \"timeout\"",[80,372,373],{"class":90},": context.pool.timeout,\n",[80,375,377],{"class":82,"line":376},19,[80,378,379],{"class":90}," },\n",[80,381,383],{"class":82,"line":382},20,[80,384,385],{"class":90}," )\n",[80,387,389],{"class":82,"line":388},21,[80,390,392],{"class":391},"sJ8bj"," # Allow default error handling to proceed\n",[48,394,396],{"id":395},"driver-specific-leak-mitigation-strategies","Driver-Specific Leak Mitigation Strategies",[14,398,399,400,403,404,153,407,153,410,413,414,417,418,421],{},"The underlying async driver dictates how connection state transitions are enforced. ",[18,401,402],{},"asyncpg"," maintains a strict connection state machine (",[18,405,406],{},"idle",[18,408,409],{},"in_transaction",[18,411,412],{},"in_copy",", etc.) and will raise explicit errors if a connection is returned to the pool while holding uncommitted transactions or open cursors. Conversely, ",[18,415,416],{},"psycopg","'s async adapter relies more heavily on ",[18,419,420],{},"libpq","'s underlying behavior, which can sometimes mask cursor leaks or leave transaction blocks open until explicit rollback.",[14,423,424,425,165],{},"For a deep dive into performance and stability trade-offs between these adapters, see ",[42,426,428],{"href":427},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fchoosing-between-asyncpg-and-psycopg-async-drivers\u002F","Choosing Between asyncpg and psycopg Async Drivers",[14,430,431,432,435],{},"Regardless of the driver, production code must enforce deterministic cleanup. The following pattern guarantees connection release even during ",[18,433,434],{},"CancelledError"," or unexpected exceptions:",[71,437,439],{"className":73,"code":438,"language":75,"meta":76,"style":76},"from contextlib import asynccontextmanager\nfrom typing import AsyncGenerator\nfrom sqlalchemy.ext.asyncio import AsyncEngine, AsyncConnection\nfrom sqlalchemy.exc import SQLAlchemyError\nimport logging\n\nlogger = logging.getLogger(__name__)\n\n@asynccontextmanager\nasync def managed_connection(engine: AsyncEngine) -> AsyncGenerator[AsyncConnection, None]:\n \"\"\"Async context manager with explicit fallback cleanup to prevent leaks.\"\"\"\n conn: AsyncConnection | None = None\n try:\n async with engine.connect() as conn:\n yield conn\n await conn.commit()\n except SQLAlchemyError as exc:\n logger.warning(\"Transaction failed, rolling back connection\", exc_info=exc)\n if conn:\n await conn.rollback()\n raise\n except BaseException:\n # Catch asyncio.CancelledError and other system exceptions\n if conn:\n await conn.rollback()\n raise\n finally:\n # Explicit close ensures the connection is returned to the pool\n if conn:\n await conn.close()\n",[18,440,441,453,465,476,487,493,497,510,514,519,538,543,560,567,584,592,600,613,631,637,644,649,659,665,672,679,684,692,698,705],{"__ignoreMap":76},[80,442,443,445,448,450],{"class":82,"line":83},[80,444,196],{"class":86},[80,446,447],{"class":90}," contextlib ",[80,449,87],{"class":86},[80,451,452],{"class":90}," asynccontextmanager\n",[80,454,455,457,460,462],{"class":82,"line":94},[80,456,196],{"class":86},[80,458,459],{"class":90}," typing ",[80,461,87],{"class":86},[80,463,464],{"class":90}," AsyncGenerator\n",[80,466,467,469,471,473],{"class":82,"line":101},[80,468,196],{"class":86},[80,470,211],{"class":90},[80,472,87],{"class":86},[80,474,475],{"class":90}," AsyncEngine, AsyncConnection\n",[80,477,478,480,482,484],{"class":82,"line":124},[80,479,196],{"class":86},[80,481,223],{"class":90},[80,483,87],{"class":86},[80,485,486],{"class":90}," SQLAlchemyError\n",[80,488,489,491],{"class":82,"line":237},[80,490,87],{"class":86},[80,492,91],{"class":90},[80,494,495],{"class":82,"line":242},[80,496,98],{"emptyLinePlaceholder":97},[80,498,499,501,503,505,508],{"class":82,"line":258},[80,500,245],{"class":90},[80,502,111],{"class":86},[80,504,250],{"class":90},[80,506,507],{"class":117},"__name__",[80,509,121],{"class":90},[80,511,512],{"class":82,"line":263},[80,513,98],{"emptyLinePlaceholder":97},[80,515,516],{"class":82,"line":278},[80,517,518],{"class":266},"@asynccontextmanager\n",[80,520,521,524,527,530,533,535],{"class":82,"line":296},[80,522,523],{"class":86},"async",[80,525,526],{"class":86}," def",[80,528,529],{"class":266}," managed_connection",[80,531,532],{"class":90},"(engine: AsyncEngine) -> AsyncGenerator[AsyncConnection, ",[80,534,290],{"class":117},[80,536,537],{"class":90},"]:\n",[80,539,540],{"class":82,"line":302},[80,541,542],{"class":130}," \"\"\"Async context manager with explicit fallback cleanup to prevent leaks.\"\"\"\n",[80,544,545,548,551,554,557],{"class":82,"line":314},[80,546,547],{"class":90}," conn: AsyncConnection ",[80,549,550],{"class":86},"|",[80,552,553],{"class":117}," None",[80,555,556],{"class":86}," =",[80,558,559],{"class":117}," None\n",[80,561,562,565],{"class":82,"line":320},[80,563,564],{"class":86}," try",[80,566,293],{"class":90},[80,568,569,572,575,578,581],{"class":82,"line":329},[80,570,571],{"class":86}," async",[80,573,574],{"class":86}," with",[80,576,577],{"class":90}," engine.connect() ",[80,579,580],{"class":86},"as",[80,582,583],{"class":90}," conn:\n",[80,585,586,589],{"class":82,"line":340},[80,587,588],{"class":86}," yield",[80,590,591],{"class":90}," conn\n",[80,593,594,597],{"class":82,"line":349},[80,595,596],{"class":86}," await",[80,598,599],{"class":90}," conn.commit()\n",[80,601,602,605,608,610],{"class":82,"line":358},[80,603,604],{"class":86}," except",[80,606,607],{"class":90}," SQLAlchemyError ",[80,609,580],{"class":86},[80,611,612],{"class":90}," exc:\n",[80,614,615,618,621,623,626,628],{"class":82,"line":367},[80,616,617],{"class":90}," logger.warning(",[80,619,620],{"class":130},"\"Transaction failed, rolling back connection\"",[80,622,153],{"class":90},[80,624,625],{"class":107},"exc_info",[80,627,111],{"class":86},[80,629,630],{"class":90},"exc)\n",[80,632,633,635],{"class":82,"line":376},[80,634,305],{"class":86},[80,636,583],{"class":90},[80,638,639,641],{"class":82,"line":382},[80,640,596],{"class":86},[80,642,643],{"class":90}," conn.rollback()\n",[80,645,646],{"class":82,"line":388},[80,647,648],{"class":86}," raise\n",[80,650,652,654,657],{"class":82,"line":651},22,[80,653,604],{"class":86},[80,655,656],{"class":117}," BaseException",[80,658,293],{"class":90},[80,660,662],{"class":82,"line":661},23,[80,663,664],{"class":391}," # Catch asyncio.CancelledError and other system exceptions\n",[80,666,668,670],{"class":82,"line":667},24,[80,669,305],{"class":86},[80,671,583],{"class":90},[80,673,675,677],{"class":82,"line":674},25,[80,676,596],{"class":86},[80,678,643],{"class":90},[80,680,682],{"class":82,"line":681},26,[80,683,648],{"class":86},[80,685,687,690],{"class":82,"line":686},27,[80,688,689],{"class":86}," finally",[80,691,293],{"class":90},[80,693,695],{"class":82,"line":694},28,[80,696,697],{"class":391}," # Explicit close ensures the connection is returned to the pool\n",[80,699,701,703],{"class":82,"line":700},29,[80,702,305],{"class":86},[80,704,583],{"class":90},[80,706,708,710],{"class":82,"line":707},30,[80,709,596],{"class":86},[80,711,712],{"class":90}," conn.close()\n",[48,714,716],{"id":715},"implementing-connection-validation-and-health-checks","Implementing Connection Validation and Health Checks",[14,718,719,720,723,724,727],{},"Network partitions, database restarts, and idle timeouts frequently leave TCP sockets in a half-closed state. SQLAlchemy 2.0 provides ",[18,721,722],{},"pool_pre_ping=True"," to execute a lightweight ",[18,725,726],{},"SELECT 1"," before checkout, discarding stale connections. In async contexts, this validation occurs within the event loop, adding measurable latency to high-throughput workloads.",[14,729,730],{},"Configuration workflows for tuning validation frequency and recycling intervals are detailed in Configuring Connection Validation on Checkout.",[14,732,733,737,738,740,741,744,745,748,749,752],{},[734,735,736],"strong",{},"Performance Trade-off:"," Enabling ",[18,739,722],{}," adds ~1-5ms per checkout depending on network RTT. For latency-sensitive APIs, prefer ",[18,742,743],{},"pool_recycle"," (e.g., ",[18,746,747],{},"1800"," seconds) combined with application-level retry logic. Reserve ",[18,750,751],{},"pool_pre_ping"," for environments with unpredictable network stability or aggressive database connection timeouts.",[71,754,756],{"className":73,"code":755,"language":75,"meta":76,"style":76},"from sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(\n \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\",\n pool_size=20,\n max_overflow=10,\n pool_timeout=15,\n pool_recycle=1800, # Recycle connections after 30 minutes\n pool_pre_ping=True, # Validate on checkout (adds latency)\n echo=False,\n)\n",[18,757,758,769,773,783,790,802,814,826,840,855,867],{"__ignoreMap":76},[80,759,760,762,764,766],{"class":82,"line":83},[80,761,196],{"class":86},[80,763,211],{"class":90},[80,765,87],{"class":86},[80,767,768],{"class":90}," create_async_engine\n",[80,770,771],{"class":82,"line":94},[80,772,98],{"emptyLinePlaceholder":97},[80,774,775,778,780],{"class":82,"line":101},[80,776,777],{"class":90},"engine ",[80,779,111],{"class":86},[80,781,782],{"class":90}," create_async_engine(\n",[80,784,785,788],{"class":82,"line":124},[80,786,787],{"class":130}," \"postgresql+asyncpg:\u002F\u002Fuser:pass@localhost\u002Fdb\"",[80,789,326],{"class":90},[80,791,792,795,797,800],{"class":82,"line":237},[80,793,794],{"class":107}," pool_size",[80,796,111],{"class":86},[80,798,799],{"class":117},"20",[80,801,326],{"class":90},[80,803,804,807,809,812],{"class":82,"line":242},[80,805,806],{"class":107}," max_overflow",[80,808,111],{"class":86},[80,810,811],{"class":117},"10",[80,813,326],{"class":90},[80,815,816,819,821,824],{"class":82,"line":258},[80,817,818],{"class":107}," pool_timeout",[80,820,111],{"class":86},[80,822,823],{"class":117},"15",[80,825,326],{"class":90},[80,827,828,831,833,835,837],{"class":82,"line":263},[80,829,830],{"class":107}," pool_recycle",[80,832,111],{"class":86},[80,834,747],{"class":117},[80,836,153],{"class":90},[80,838,839],{"class":391},"# Recycle connections after 30 minutes\n",[80,841,842,845,847,850,852],{"class":82,"line":278},[80,843,844],{"class":107}," pool_pre_ping",[80,846,111],{"class":86},[80,848,849],{"class":117},"True",[80,851,153],{"class":90},[80,853,854],{"class":391},"# Validate on checkout (adds latency)\n",[80,856,857,860,862,865],{"class":82,"line":296},[80,858,859],{"class":107}," echo",[80,861,111],{"class":86},[80,863,864],{"class":117},"False",[80,866,326],{"class":90},[80,868,869],{"class":82,"line":302},[80,870,121],{"class":90},[48,872,874],{"id":873},"resilience-patterns-for-high-availability-topologies","Resilience Patterns for High-Availability Topologies",[14,876,877,878,880],{},"In primary\u002Freplica or multi-AZ deployments, connection drops during failovers or network partitions can exhaust the pool if the application blindly retries without backoff. SQLAlchemy's connection pool does not automatically detect topology changes; it will continue attempting to use invalidated connections until ",[18,879,32],{}," triggers.",[14,882,883],{},"Implement exponential backoff and circuit breakers around session acquisition. Cross-reference topology-aware pooling strategies in Handling Database Failover in Async Connection Pools.",[14,885,886],{},"Event listener hooks can automate pool resets when topology shifts are detected:",[71,888,890],{"className":73,"code":889,"language":75,"meta":76,"style":76},"from sqlalchemy import event\nfrom sqlalchemy.pool import Pool\nimport asyncio\n\n@event.listens_for(Pool, \"reset\")\ndef on_pool_reset(pool: Pool, dbapi_connection: object, connection_record: object) -> None:\n \"\"\"Log pool resets triggered by failover detection or explicit invalidate calls.\"\"\"\n logger.info(\"Connection pool reset initiated. Active connections: %d\", pool.checkedout())\n\nasync def handle_failover_recovery(engine: AsyncEngine) -> None:\n \"\"\"Gracefully invalidate pool after confirmed topology change.\"\"\"\n await engine.dispose()\n # Re-initialize engine or trigger application-level health check\n logger.info(\"Async engine disposed. Awaiting topology stabilization.\")\n",[18,891,892,902,914,921,925,937,962,967,984,988,1004,1009,1016,1021],{"__ignoreMap":76},[80,893,894,896,898,900],{"class":82,"line":83},[80,895,196],{"class":86},[80,897,199],{"class":90},[80,899,87],{"class":86},[80,901,204],{"class":90},[80,903,904,906,909,911],{"class":82,"line":94},[80,905,196],{"class":86},[80,907,908],{"class":90}," sqlalchemy.pool ",[80,910,87],{"class":86},[80,912,913],{"class":90}," Pool\n",[80,915,916,918],{"class":82,"line":101},[80,917,87],{"class":86},[80,919,920],{"class":90}," asyncio\n",[80,922,923],{"class":82,"line":124},[80,924,98],{"emptyLinePlaceholder":97},[80,926,927,929,932,935],{"class":82,"line":237},[80,928,267],{"class":266},[80,930,931],{"class":90},"(Pool, ",[80,933,934],{"class":130},"\"reset\"",[80,936,121],{"class":90},[80,938,939,941,944,947,950,953,955,958,960],{"class":82,"line":242},[80,940,281],{"class":86},[80,942,943],{"class":266}," on_pool_reset",[80,945,946],{"class":90},"(pool: Pool, dbapi_connection: ",[80,948,949],{"class":117},"object",[80,951,952],{"class":90},", connection_record: ",[80,954,949],{"class":117},[80,956,957],{"class":90},") -> ",[80,959,290],{"class":117},[80,961,293],{"class":90},[80,963,964],{"class":82,"line":258},[80,965,966],{"class":130}," \"\"\"Log pool resets triggered by failover detection or explicit invalidate calls.\"\"\"\n",[80,968,969,972,975,978,981],{"class":82,"line":263},[80,970,971],{"class":90}," logger.info(",[80,973,974],{"class":130},"\"Connection pool reset initiated. Active connections: ",[80,976,977],{"class":117},"%d",[80,979,980],{"class":130},"\"",[80,982,983],{"class":90},", pool.checkedout())\n",[80,985,986],{"class":82,"line":278},[80,987,98],{"emptyLinePlaceholder":97},[80,989,990,992,994,997,1000,1002],{"class":82,"line":296},[80,991,523],{"class":86},[80,993,526],{"class":86},[80,995,996],{"class":266}," handle_failover_recovery",[80,998,999],{"class":90},"(engine: AsyncEngine) -> ",[80,1001,290],{"class":117},[80,1003,293],{"class":90},[80,1005,1006],{"class":82,"line":302},[80,1007,1008],{"class":130}," \"\"\"Gracefully invalidate pool after confirmed topology change.\"\"\"\n",[80,1010,1011,1013],{"class":82,"line":314},[80,1012,596],{"class":86},[80,1014,1015],{"class":90}," engine.dispose()\n",[80,1017,1018],{"class":82,"line":320},[80,1019,1020],{"class":391}," # Re-initialize engine or trigger application-level health check\n",[80,1022,1023,1025,1028],{"class":82,"line":329},[80,1024,971],{"class":90},[80,1026,1027],{"class":130},"\"Async engine disposed. Awaiting topology stabilization.\"",[80,1029,121],{"class":90},[48,1031,1033],{"id":1032},"secure-credential-rotation-without-pool-disruption","Secure Credential Rotation Without Pool Disruption",[14,1035,1036,1037,1040,1041,1043],{},"Modern infrastructure mandates automated credential rotation via HashiCorp Vault or AWS Secrets Manager. Dropping and recreating the entire ",[18,1038,1039],{},"AsyncEngine"," during rotation causes transient ",[18,1042,32],{}," spikes. Instead, implement dynamic connection string resolution while preserving active pool states.",[14,1045,1046],{},"Credential lifecycle management patterns are covered in Managing Database Credentials in Async Environments.",[14,1048,1049,1050,1053],{},"Zero-downtime rotation requires a custom ",[18,1051,1052],{},"poolclass"," or a factory that swaps the URL on new checkouts while allowing existing connections to drain naturally:",[71,1055,1057],{"className":73,"code":1056,"language":75,"meta":76,"style":76},"from typing import Callable, Awaitable\nfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine\nfrom sqlalchemy.pool import QueuePool\nimport asyncio\n\nclass DynamicCredentialPool(QueuePool):\n \"\"\"Custom pool that fetches fresh credentials on overflow or recycle.\"\"\"\n \n def __init__(self, credential_fetcher: Callable[[], Awaitable[str]], *args, **kwargs):\n self._credential_fetcher = credential_fetcher\n super().__init__(*args, **kwargs)\n\n def _create_connection(self) -> object:\n # In production, integrate with async Vault\u002FAWS SDK here\n # This example assumes synchronous fetch for brevity; use asyncio.to_thread() in prod\n new_url = asyncio.get_event_loop().run_until_complete(self._credential_fetcher())\n # Override connection creation with fresh URL\n return super()._create_connection()\n\nasync def get_fresh_credentials() -> str:\n # Simulate async secret retrieval\n return \"postgresql+asyncpg:\u002F\u002Fuser:rotated_pass@localhost\u002Fdb\"\n\nasync def build_resilient_engine() -> AsyncEngine:\n return create_async_engine(\n \"postgresql+asyncpg:\u002F\u002Fplaceholder:placeholder@localhost\u002Fdb\",\n poolclass=DynamicCredentialPool,\n pool_size=15,\n max_overflow=5,\n pool_recycle=300,\n connect_args={\"credential_fetcher\": get_fresh_credentials},\n )\n",[18,1058,1059,1070,1081,1092,1098,1102,1119,1124,1129,1157,1170,1192,1196,1210,1215,1220,1236,1241,1251,1255,1271,1276,1283,1287,1299,1305,1312,1322,1332,1343,1354,1371],{"__ignoreMap":76},[80,1060,1061,1063,1065,1067],{"class":82,"line":83},[80,1062,196],{"class":86},[80,1064,459],{"class":90},[80,1066,87],{"class":86},[80,1068,1069],{"class":90}," Callable, Awaitable\n",[80,1071,1072,1074,1076,1078],{"class":82,"line":94},[80,1073,196],{"class":86},[80,1075,211],{"class":90},[80,1077,87],{"class":86},[80,1079,1080],{"class":90}," create_async_engine, AsyncEngine\n",[80,1082,1083,1085,1087,1089],{"class":82,"line":101},[80,1084,196],{"class":86},[80,1086,908],{"class":90},[80,1088,87],{"class":86},[80,1090,1091],{"class":90}," QueuePool\n",[80,1093,1094,1096],{"class":82,"line":124},[80,1095,87],{"class":86},[80,1097,920],{"class":90},[80,1099,1100],{"class":82,"line":237},[80,1101,98],{"emptyLinePlaceholder":97},[80,1103,1104,1107,1110,1113,1116],{"class":82,"line":242},[80,1105,1106],{"class":86},"class",[80,1108,1109],{"class":266}," DynamicCredentialPool",[80,1111,1112],{"class":90},"(",[80,1114,1115],{"class":266},"QueuePool",[80,1117,1118],{"class":90},"):\n",[80,1120,1121],{"class":82,"line":258},[80,1122,1123],{"class":130}," \"\"\"Custom pool that fetches fresh credentials on overflow or recycle.\"\"\"\n",[80,1125,1126],{"class":82,"line":263},[80,1127,1128],{"class":90}," \n",[80,1130,1131,1133,1136,1139,1142,1145,1148,1151,1154],{"class":82,"line":278},[80,1132,526],{"class":86},[80,1134,1135],{"class":117}," __init__",[80,1137,1138],{"class":90},"(self, credential_fetcher: Callable[[], Awaitable[",[80,1140,1141],{"class":117},"str",[80,1143,1144],{"class":90},"]], ",[80,1146,1147],{"class":86},"*",[80,1149,1150],{"class":90},"args, ",[80,1152,1153],{"class":86},"**",[80,1155,1156],{"class":90},"kwargs):\n",[80,1158,1159,1162,1165,1167],{"class":82,"line":296},[80,1160,1161],{"class":117}," self",[80,1163,1164],{"class":90},"._credential_fetcher ",[80,1166,111],{"class":86},[80,1168,1169],{"class":90}," credential_fetcher\n",[80,1171,1172,1175,1178,1181,1183,1185,1187,1189],{"class":82,"line":302},[80,1173,1174],{"class":117}," super",[80,1176,1177],{"class":90},"().",[80,1179,1180],{"class":117},"__init__",[80,1182,1112],{"class":90},[80,1184,1147],{"class":86},[80,1186,1150],{"class":90},[80,1188,1153],{"class":86},[80,1190,1191],{"class":90},"kwargs)\n",[80,1193,1194],{"class":82,"line":314},[80,1195,98],{"emptyLinePlaceholder":97},[80,1197,1198,1200,1203,1206,1208],{"class":82,"line":320},[80,1199,526],{"class":86},[80,1201,1202],{"class":266}," _create_connection",[80,1204,1205],{"class":90},"(self) -> ",[80,1207,949],{"class":117},[80,1209,293],{"class":90},[80,1211,1212],{"class":82,"line":329},[80,1213,1214],{"class":391}," # In production, integrate with async Vault\u002FAWS SDK here\n",[80,1216,1217],{"class":82,"line":340},[80,1218,1219],{"class":391}," # This example assumes synchronous fetch for brevity; use asyncio.to_thread() in prod\n",[80,1221,1222,1225,1227,1230,1233],{"class":82,"line":349},[80,1223,1224],{"class":90}," new_url ",[80,1226,111],{"class":86},[80,1228,1229],{"class":90}," asyncio.get_event_loop().run_until_complete(",[80,1231,1232],{"class":117},"self",[80,1234,1235],{"class":90},"._credential_fetcher())\n",[80,1237,1238],{"class":82,"line":358},[80,1239,1240],{"class":391}," # Override connection creation with fresh URL\n",[80,1242,1243,1246,1248],{"class":82,"line":367},[80,1244,1245],{"class":86}," return",[80,1247,1174],{"class":117},[80,1249,1250],{"class":90},"()._create_connection()\n",[80,1252,1253],{"class":82,"line":376},[80,1254,98],{"emptyLinePlaceholder":97},[80,1256,1257,1259,1261,1264,1267,1269],{"class":82,"line":382},[80,1258,523],{"class":86},[80,1260,526],{"class":86},[80,1262,1263],{"class":266}," get_fresh_credentials",[80,1265,1266],{"class":90},"() -> ",[80,1268,1141],{"class":117},[80,1270,293],{"class":90},[80,1272,1273],{"class":82,"line":388},[80,1274,1275],{"class":391}," # Simulate async secret retrieval\n",[80,1277,1278,1280],{"class":82,"line":651},[80,1279,1245],{"class":86},[80,1281,1282],{"class":130}," \"postgresql+asyncpg:\u002F\u002Fuser:rotated_pass@localhost\u002Fdb\"\n",[80,1284,1285],{"class":82,"line":661},[80,1286,98],{"emptyLinePlaceholder":97},[80,1288,1289,1291,1293,1296],{"class":82,"line":667},[80,1290,523],{"class":86},[80,1292,526],{"class":86},[80,1294,1295],{"class":266}," build_resilient_engine",[80,1297,1298],{"class":90},"() -> AsyncEngine:\n",[80,1300,1301,1303],{"class":82,"line":674},[80,1302,1245],{"class":86},[80,1304,782],{"class":90},[80,1306,1307,1310],{"class":82,"line":681},[80,1308,1309],{"class":130}," \"postgresql+asyncpg:\u002F\u002Fplaceholder:placeholder@localhost\u002Fdb\"",[80,1311,326],{"class":90},[80,1313,1314,1317,1319],{"class":82,"line":686},[80,1315,1316],{"class":107}," poolclass",[80,1318,111],{"class":86},[80,1320,1321],{"class":90},"DynamicCredentialPool,\n",[80,1323,1324,1326,1328,1330],{"class":82,"line":694},[80,1325,794],{"class":107},[80,1327,111],{"class":86},[80,1329,823],{"class":117},[80,1331,326],{"class":90},[80,1333,1334,1336,1338,1341],{"class":82,"line":700},[80,1335,806],{"class":107},[80,1337,111],{"class":86},[80,1339,1340],{"class":117},"5",[80,1342,326],{"class":90},[80,1344,1345,1347,1349,1352],{"class":82,"line":707},[80,1346,830],{"class":107},[80,1348,111],{"class":86},[80,1350,1351],{"class":117},"300",[80,1353,326],{"class":90},[80,1355,1357,1360,1362,1365,1368],{"class":82,"line":1356},31,[80,1358,1359],{"class":107}," connect_args",[80,1361,111],{"class":86},[80,1363,1364],{"class":90},"{",[80,1366,1367],{"class":130},"\"credential_fetcher\"",[80,1369,1370],{"class":90},": get_fresh_credentials},\n",[80,1372,1374],{"class":82,"line":1373},32,[80,1375,385],{"class":90},[48,1377,1379],{"id":1378},"common-pitfalls","Common Pitfalls",[1381,1382,1383,1406,1423,1438,1444,1453],"ul",{},[1384,1385,1386,1397,1398,1401,1402,1405],"li",{},[734,1387,1388,1389,1392,1393,1396],{},"Using synchronous ",[18,1390,1391],{},"Session.commit()"," inside ",[18,1394,1395],{},"async def"," routes"," without ",[18,1399,1400],{},"run_sync()"," or ",[18,1403,1404],{},"AsyncSession",", blocking the event loop and masking true concurrency.",[1384,1407,1408,1422],{},[734,1409,1410,1411,1414,1415,1401,1418,1421],{},"Omitting ",[18,1412,1413],{},"await"," on ",[18,1416,1417],{},"session.close()",[18,1419,1420],{},"conn.release()",","," leaving transactions open and permanently blocking pool slots.",[1384,1424,1425,1434,1435,1437],{},[734,1426,1427,1428,1430,1431,1421],{},"Setting ",[18,1429,152],{}," too low while ",[18,1432,1433],{},"max_overflow=0"," causing immediate ",[18,1436,32],{}," under concurrent load spikes.",[1384,1439,1440,1443],{},[734,1441,1442],{},"Relying on Python garbage collection"," for connection cleanup instead of deterministic async context managers.",[1384,1445,1446,1452],{},[734,1447,1448,1449],{},"Failing to handle ",[18,1450,1451],{},"asyncpg.exceptions.ConnectionDoesNotExistError"," during transient network blips, causing silent connection drops.",[1384,1454,1455,1458],{},[734,1456,1457],{},"Mixing sync and async dialects"," in the same application, causing event loop blocking and false leak reports.",[48,1460,1462],{"id":1461},"frequently-asked-questions","Frequently Asked Questions",[14,1464,1465,1468,1469,1471,1472,1474,1475,1477,1478,1480],{},[734,1466,1467],{},"How do I detect a connection leak in SQLAlchemy 2.0 async?","\nMonitor ",[18,1470,32],{}," exceptions, track ",[18,1473,171],{}," metrics, and enable ",[18,1476,118],{}," logging on ",[18,1479,68],{}," to trace unchecked-out connections against active tasks. Correlate checkout timestamps with long-running queries.",[14,1482,1483,1486],{},[734,1484,1485],{},"What is the difference between pool exhaustion and connection leaks?","\nPool exhaustion occurs when all available connections are legitimately in use under high concurrency. Connection leaks happen when connections are acquired but never returned due to missing cleanup, unhandled exceptions, or unawaited coroutines.",[14,1488,1489,1495,1496,1498,1499,1501],{},[734,1490,1491,1492,1494],{},"Can I safely increase ",[18,1493,152],{}," to fix exhaustion?","\nOnly if the database server can handle the concurrent connections. Blindly increasing ",[18,1497,152],{}," shifts the bottleneck to database CPU\u002Fmemory. Optimize query concurrency, implement connection validation, and use ",[18,1500,156],{}," for temporary spikes first.",[14,1503,1504,1507,1509,1510,1512,1513,1515,1516,1518,1519,1522],{},[734,1505,1506],{},"How does asyncpg handle connection recycling differently than psycopg?",[18,1508,402],{}," uses a strict connection pool with explicit state management and requires ",[18,1511,743],{}," or pre-ping for stale connections. ",[18,1514,416],{},"'s async adapter relies more on underlying ",[18,1517,420],{}," behavior and may require explicit ",[18,1520,1521],{},"reset"," calls after network interruptions.",[1524,1525,1526],"style",{},"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 .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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":76,"searchDepth":94,"depth":94,"links":1528},[1529,1530,1531,1532,1533,1534,1535],{"id":50,"depth":94,"text":51},{"id":395,"depth":94,"text":396},{"id":715,"depth":94,"text":716},{"id":873,"depth":94,"text":874},{"id":1032,"depth":94,"text":1033},{"id":1378,"depth":94,"text":1379},{"id":1461,"depth":94,"text":1462},"In asynchronous Python applications, the database connection lifecycle is strictly bound to the event loop rather than thread-local state. Unlike synchronous SQLAlchemy, where deterministic cleanup often relies on garbage collection or explicit close() calls in finally blocks, async execution introduces critical boundaries: unawaited coroutines, missing async with context managers, and unhandled exceptions can silently retain connections, triggering pool starvation. Understanding these async\u002Fawait boundaries is foundational to preventing PoolTimeout errors in production.","md",{},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion",{"title":5,"description":1536},"async-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Findex","ghHCFCKv3NN16ez37KkPBNVcRsMuGkI-nSOIM-wnGkM",1778149144400]