[{"data":1,"prerenderedAt":2454},["ShallowReactive",2],{"page-\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Ffixing-connection-pool-exhaustion\u002F":3,"faq-schema-\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Ffixing-connection-pool-exhaustion\u002F":2433},{"id":4,"title":5,"body":6,"description":2423,"extension":2424,"meta":2425,"navigation":286,"path":2429,"seo":2430,"stem":2431,"__hash__":2432},"content\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Ffixing-connection-pool-exhaustion\u002Findex.md","Fixing Connection Pool Exhaustion in a Production Python API",{"type":7,"value":8,"toc":2412},"minimark",[9,13,27,34,39,57,60,222,225,630,645,649,660,667,787,790,1034,1045,1049,1060,1149,1161,1582,1614,1618,1632,1743,1970,1985,1988,2130,2134,2140,2207,2215,2219,2222,2277,2289,2293,2307,2310,2314,2323,2332,2338,2347,2353,2357,2362,2388,2393,2408],[10,11,5],"h1",{"id":12},"fixing-connection-pool-exhaustion-in-a-production-python-api",[14,15,16,20,21,26],"p",{},[17,18,19],"code",{},"TimeoutError: QueuePool limit of size 5 overflow 10 reached, connection timed out, timeout 30.00"," is the error that turns a good launch day into a bad one. Your API was fine at ten requests a second and now every endpoint hangs for thirty seconds and then returns a 500, including the health check, which means your host starts killing containers and replacing them with fresh ones that immediately do the same thing. This page walks the whole diagnosis: what the message actually says, the arithmetic that decides how many connections you may open, the leak that causes most real cases, and the timeout settings that turn a hang into a fast, honest failure. It is part of the ",[22,23,25],"a",{"href":24},"\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002F","Async Database Access with SQLAlchemy"," guide.",[14,28,29,30,33],{},"The decision this page resolves is the one everybody gets wrong under pressure: raise ",[17,31,32],{},"pool_size",", or find the leak. Raising the pool is the instinct, it works for about forty minutes, and then it fails harder because you have moved the bottleneck onto the database server where recovery is slower. Find the leak first. Almost always there is one.",[35,36,38],"h2",{"id":37},"what-the-error-is-really-telling-you","What the error is really telling you",[14,40,41,42,45,46,48,49,52,53,56],{},"SQLAlchemy's ",[17,43,44],{},"QueuePool"," is a fixed set of connection slots per process. ",[17,47,32],{}," is the number kept open permanently. ",[17,50,51],{},"max_overflow"," is how many extra connections it may open temporarily under load, closing them again when they go idle. Total capacity per process is the sum. When every slot is checked out, the next coroutine that asks for a session does not get an error — it waits, quietly, for up to ",[17,54,55],{},"pool_timeout"," seconds. Only after that does it raise.",[14,58,59],{},"That waiting is why exhaustion feels like a mystery outage rather than a database problem. Latency goes vertical, CPU sits near zero, the database reports a handful of active queries, and nothing in the logs mentions the database until the timeouts start. The system is not busy. It is queueing.",[61,62,70,71,70,75,70,79,70,86,70,94,70,103,70,109,70,113,70,119,70,125,70,132,70,135,70,138,70,141,70,144,70,147,70,151,70,153,70,155,70,157,70,159,70,164,70,168,70,172,70,179,70,183,70,189,70,194,70,198,70,203,70,206,70,210,70,214,70,218],"svg",{"viewBox":63,"role":64,"ariaLabelledBy":65,"xmlns":68,"style":69},"0 0 760 360","img",[66,67],"cpx-queue-t","cpx-queue-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[72,73,74],"title",{"id":66},"How a checkout queue builds and times out",[76,77,78],"desc",{"id":67},"Twenty-four concurrent requests contend for ten pool slots; the remaining fourteen wait on the checkout queue and raise a timeout after the pool timeout elapses.",[80,81],"rect",{"x":82,"y":82,"width":83,"height":84,"fill":85},"0","760","360","var(--c-surface)",[87,88,93],"text",{"x":89,"y":90,"fill":91,"style":92},"380","26","var(--c-text)","text-anchor:middle;font-size:14;font-family:var(--font-sans);","One worker process: 24 in-flight requests, 10 slots",[80,95],{"x":90,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":101,"style":102},"58","150","92","8","var(--c-surface-alt)","var(--c-blue)","stroke-width:2;",[87,104,108],{"x":105,"y":106,"fill":91,"style":107},"101","96","text-anchor:middle;font-size:12;font-family:var(--font-sans);","Event loop",[87,110,112],{"x":105,"y":111,"fill":91,"style":107},"118","24 coroutines",[87,114,118],{"x":105,"y":115,"fill":116,"style":117},"138","var(--c-text-muted)","text-anchor:middle;font-size:11;font-family:var(--font-sans);","all want a session",[87,120,124],{"x":121,"y":122,"fill":116,"style":123},"230","52","font-size:11;font-family:var(--font-sans);","pool_size 5 (persistent)",[80,126],{"x":121,"y":127,"width":128,"height":129,"rx":130,"fill":85,"stroke":131,"style":102},"60","48","34","5","var(--c-teal)",[80,133],{"x":134,"y":127,"width":128,"height":129,"rx":130,"fill":85,"stroke":131,"style":102},"286",[80,136],{"x":137,"y":127,"width":128,"height":129,"rx":130,"fill":85,"stroke":131,"style":102},"342",[80,139],{"x":140,"y":127,"width":128,"height":129,"rx":130,"fill":85,"stroke":131,"style":102},"398",[80,142],{"x":143,"y":127,"width":128,"height":129,"rx":130,"fill":85,"stroke":131,"style":102},"454",[87,145,146],{"x":121,"y":111,"fill":116,"style":123},"max_overflow 5 (temporary)",[80,148],{"x":121,"y":149,"width":128,"height":129,"rx":130,"fill":85,"stroke":150,"style":102},"126","var(--c-yellow)",[80,152],{"x":134,"y":149,"width":128,"height":129,"rx":130,"fill":85,"stroke":150,"style":102},[80,154],{"x":137,"y":149,"width":128,"height":129,"rx":130,"fill":85,"stroke":150,"style":102},[80,156],{"x":140,"y":149,"width":128,"height":129,"rx":130,"fill":85,"stroke":150,"style":102},[80,158],{"x":143,"y":149,"width":128,"height":129,"rx":130,"fill":85,"stroke":150,"style":102},[80,160],{"x":161,"y":96,"width":162,"height":163,"rx":99,"fill":100,"stroke":101,"style":102},"560","174","102",[87,165,167],{"x":166,"y":106,"fill":91,"style":107},"647","Postgres backends",[87,169,171],{"x":166,"y":170,"fill":91,"style":107},"120","10 busy",[173,174],"line",{"x1":175,"y1":176,"x2":177,"y2":178,"stroke":116,"style":102},"176","86","224","78",[173,180],{"x1":181,"y1":178,"x2":182,"y2":98,"stroke":116,"style":102},"502","556",[80,184],{"x":90,"y":185,"width":186,"height":187,"rx":99,"fill":100,"stroke":188,"style":102},"204","476","64","var(--c-border)",[87,190,193],{"x":191,"y":192,"fill":91,"style":107},"264","232","14 requests parked on the checkout queue",[87,195,197],{"x":191,"y":196,"fill":116,"style":117},"254","no error yet, no CPU used, latency climbing",[173,199],{"x1":181,"y1":200,"x2":201,"y2":200,"stroke":202,"style":102},"236","552","var(--c-coral)",[80,204],{"x":205,"y":185,"width":175,"height":187,"rx":99,"fill":85,"stroke":202,"style":102},"558",[87,207,209],{"x":208,"y":121,"fill":91,"style":107},"646","After pool_timeout",[87,211,213],{"x":208,"y":212,"fill":91,"style":107},"252","QueuePool limit reached",[87,215,217],{"x":89,"y":216,"fill":91,"style":107},"308","The pool is a queue before it is an error: p95 latency spikes first, 500s arrive later",[87,219,221],{"x":89,"y":220,"fill":116,"style":117},"334","Alert on checked-out slots, not on the exception",[14,223,224],{},"Before you change a setting, get the pool to describe itself. SQLAlchemy exposes live counters on the pool object and emits checkout events you can time. Twenty lines of instrumentation tell you in ten minutes what a day of guessing will not.",[226,227,232],"pre",{"className":228,"code":229,"language":230,"meta":231,"style":231},"language-python shiki shiki-themes github-light github-dark","import os\nimport time\nfrom sqlalchemy import event\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nengine = create_async_engine(os.environ[\"DATABASE_URL\"])\nSLOW_HOLD_SECONDS = float(os.getenv(\"DB_SLOW_HOLD_SECONDS\", \"2.0\"))\n\n@event.listens_for(engine.sync_engine, \"checkout\")\ndef _on_checkout(dbapi_conn, record, proxy) -> None:\n    record.info[\"checked_out_at\"] = time.monotonic()\n\n@event.listens_for(engine.sync_engine, \"checkin\")\ndef _on_checkin(dbapi_conn, record) -> None:\n    started = record.info.pop(\"checked_out_at\", None)\n    if started is None:\n        return\n    held = time.monotonic() - started\n    if held > SLOW_HOLD_SECONDS:\n        print(f\"connection held {held:.2f}s — suspect a leak or a slow query\")\n\ndef pool_stats() -> dict[str, int]:\n    \"\"\"Expose on an internal route; scrape it every 15 seconds.\"\"\"\n    pool = engine.pool\n    return {\n        \"size\": pool.size(),\n        \"checked_out\": pool.checkedout(),\n        \"overflow\": pool.overflow(),\n        \"waiters\": pool.checkedout() - pool.size(),\n    }\n","python","",[17,233,234,246,254,268,281,288,307,335,340,356,374,391,396,408,423,442,459,465,482,498,530,535,557,563,574,583,592,601,610,624],{"__ignoreMap":231},[235,236,238,242],"span",{"class":173,"line":237},1,[235,239,241],{"class":240},"szBVR","import",[235,243,245],{"class":244},"sVt8B"," os\n",[235,247,249,251],{"class":173,"line":248},2,[235,250,241],{"class":240},[235,252,253],{"class":244}," time\n",[235,255,257,260,263,265],{"class":173,"line":256},3,[235,258,259],{"class":240},"from",[235,261,262],{"class":244}," sqlalchemy ",[235,264,241],{"class":240},[235,266,267],{"class":244}," event\n",[235,269,271,273,276,278],{"class":173,"line":270},4,[235,272,259],{"class":240},[235,274,275],{"class":244}," sqlalchemy.ext.asyncio ",[235,277,241],{"class":240},[235,279,280],{"class":244}," create_async_engine\n",[235,282,284],{"class":173,"line":283},5,[235,285,287],{"emptyLinePlaceholder":286},true,"\n",[235,289,291,294,297,300,304],{"class":173,"line":290},6,[235,292,293],{"class":244},"engine ",[235,295,296],{"class":240},"=",[235,298,299],{"class":244}," create_async_engine(os.environ[",[235,301,303],{"class":302},"sZZnC","\"DATABASE_URL\"",[235,305,306],{"class":244},"])\n",[235,308,310,314,317,320,323,326,329,332],{"class":173,"line":309},7,[235,311,313],{"class":312},"sj4cs","SLOW_HOLD_SECONDS",[235,315,316],{"class":240}," =",[235,318,319],{"class":312}," float",[235,321,322],{"class":244},"(os.getenv(",[235,324,325],{"class":302},"\"DB_SLOW_HOLD_SECONDS\"",[235,327,328],{"class":244},", ",[235,330,331],{"class":302},"\"2.0\"",[235,333,334],{"class":244},"))\n",[235,336,338],{"class":173,"line":337},8,[235,339,287],{"emptyLinePlaceholder":286},[235,341,343,347,350,353],{"class":173,"line":342},9,[235,344,346],{"class":345},"sScJk","@event.listens_for",[235,348,349],{"class":244},"(engine.sync_engine, ",[235,351,352],{"class":302},"\"checkout\"",[235,354,355],{"class":244},")\n",[235,357,359,362,365,368,371],{"class":173,"line":358},10,[235,360,361],{"class":240},"def",[235,363,364],{"class":345}," _on_checkout",[235,366,367],{"class":244},"(dbapi_conn, record, proxy) -> ",[235,369,370],{"class":312},"None",[235,372,373],{"class":244},":\n",[235,375,377,380,383,386,388],{"class":173,"line":376},11,[235,378,379],{"class":244},"    record.info[",[235,381,382],{"class":302},"\"checked_out_at\"",[235,384,385],{"class":244},"] ",[235,387,296],{"class":240},[235,389,390],{"class":244}," time.monotonic()\n",[235,392,394],{"class":173,"line":393},12,[235,395,287],{"emptyLinePlaceholder":286},[235,397,399,401,403,406],{"class":173,"line":398},13,[235,400,346],{"class":345},[235,402,349],{"class":244},[235,404,405],{"class":302},"\"checkin\"",[235,407,355],{"class":244},[235,409,411,413,416,419,421],{"class":173,"line":410},14,[235,412,361],{"class":240},[235,414,415],{"class":345}," _on_checkin",[235,417,418],{"class":244},"(dbapi_conn, record) -> ",[235,420,370],{"class":312},[235,422,373],{"class":244},[235,424,426,429,431,434,436,438,440],{"class":173,"line":425},15,[235,427,428],{"class":244},"    started ",[235,430,296],{"class":240},[235,432,433],{"class":244}," record.info.pop(",[235,435,382],{"class":302},[235,437,328],{"class":244},[235,439,370],{"class":312},[235,441,355],{"class":244},[235,443,445,448,451,454,457],{"class":173,"line":444},16,[235,446,447],{"class":240},"    if",[235,449,450],{"class":244}," started ",[235,452,453],{"class":240},"is",[235,455,456],{"class":312}," None",[235,458,373],{"class":244},[235,460,462],{"class":173,"line":461},17,[235,463,464],{"class":240},"        return\n",[235,466,468,471,473,476,479],{"class":173,"line":467},18,[235,469,470],{"class":244},"    held ",[235,472,296],{"class":240},[235,474,475],{"class":244}," time.monotonic() ",[235,477,478],{"class":240},"-",[235,480,481],{"class":244}," started\n",[235,483,485,487,490,493,496],{"class":173,"line":484},19,[235,486,447],{"class":240},[235,488,489],{"class":244}," held ",[235,491,492],{"class":240},">",[235,494,495],{"class":312}," SLOW_HOLD_SECONDS",[235,497,373],{"class":244},[235,499,501,504,507,510,513,516,519,522,525,528],{"class":173,"line":500},20,[235,502,503],{"class":312},"        print",[235,505,506],{"class":244},"(",[235,508,509],{"class":240},"f",[235,511,512],{"class":302},"\"connection held ",[235,514,515],{"class":312},"{",[235,517,518],{"class":244},"held",[235,520,521],{"class":240},":.2f",[235,523,524],{"class":312},"}",[235,526,527],{"class":302},"s — suspect a leak or a slow query\"",[235,529,355],{"class":244},[235,531,533],{"class":173,"line":532},21,[235,534,287],{"emptyLinePlaceholder":286},[235,536,538,540,543,546,549,551,554],{"class":173,"line":537},22,[235,539,361],{"class":240},[235,541,542],{"class":345}," pool_stats",[235,544,545],{"class":244},"() -> dict[",[235,547,548],{"class":312},"str",[235,550,328],{"class":244},[235,552,553],{"class":312},"int",[235,555,556],{"class":244},"]:\n",[235,558,560],{"class":173,"line":559},23,[235,561,562],{"class":302},"    \"\"\"Expose on an internal route; scrape it every 15 seconds.\"\"\"\n",[235,564,566,569,571],{"class":173,"line":565},24,[235,567,568],{"class":244},"    pool ",[235,570,296],{"class":240},[235,572,573],{"class":244}," engine.pool\n",[235,575,577,580],{"class":173,"line":576},25,[235,578,579],{"class":240},"    return",[235,581,582],{"class":244}," {\n",[235,584,586,589],{"class":173,"line":585},26,[235,587,588],{"class":302},"        \"size\"",[235,590,591],{"class":244},": pool.size(),\n",[235,593,595,598],{"class":173,"line":594},27,[235,596,597],{"class":302},"        \"checked_out\"",[235,599,600],{"class":244},": pool.checkedout(),\n",[235,602,604,607],{"class":173,"line":603},28,[235,605,606],{"class":302},"        \"overflow\"",[235,608,609],{"class":244},": pool.overflow(),\n",[235,611,613,616,619,621],{"class":173,"line":612},29,[235,614,615],{"class":302},"        \"waiters\"",[235,617,618],{"class":244},": pool.checkedout() ",[235,620,478],{"class":240},[235,622,623],{"class":244}," pool.size(),\n",[235,625,627],{"class":173,"line":626},30,[235,628,629],{"class":244},"    }\n",[14,631,632,633,636,637,639,640,644],{},"Read the two numbers together. If ",[17,634,635],{},"checked_out"," sits at maximum while your database reports only two or three active queries, connections are being held by code that is not querying — that is a leak. If ",[17,638,635],{}," is at maximum and the database shows the same number of long-running queries, your pool is fine and your queries are slow. Those two diagnoses lead to opposite fixes, and guessing between them is what makes these incidents last for days. Pipe the counters into your existing pipeline; ",[22,641,643],{"href":642},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002F","structured logging with structlog"," makes them queryable rather than decorative.",[35,646,648],{"id":647},"the-per-worker-maths-nobody-does-until-it-breaks","The per-worker maths nobody does until it breaks",[14,650,651,652,655,656,659],{},"Here is the arithmetic that catches every builder exactly once. SQLAlchemy's pool is per process, not per container and certainly not per application. Run four Uvicorn workers behind Gunicorn and you have four independent pools. Scale to three containers and you have twelve. With the default-ish ",[17,653,654],{},"pool_size=5, max_overflow=5",", that fleet demands 120 backend connections from a Postgres whose ",[17,657,658],{},"max_connections"," is 100, three of which are reserved for superusers.",[14,661,662,663,666],{},"The database does not queue politely like SQLAlchemy does. It refuses: ",[17,664,665],{},"FATAL: sorry, too many clients already",". And it refuses your migration job and your psql session too, which is how a capacity problem becomes an outage you cannot log into.",[61,668,70,672,70,675,70,678,70,680,70,684,70,687,70,690,70,694,70,697,70,700,70,703,70,707,70,711,70,715,70,719,70,723,70,727,70,733,70,737,70,741,70,746,70,751,70,755,70,759,70,764,70,767,70,771,70,774,70,777,70,780,70,783],{"viewBox":63,"role":64,"ariaLabelledBy":669,"xmlns":68,"style":69},[670,671],"cpx-math-t","cpx-math-d",[72,673,674],{"id":670},"Connection demand as containers scale",[76,676,677],{"id":671},"Bar chart of total backend connections demanded by one to four containers running four workers with ten slots each, against an application budget of 87 connections.",[80,679],{"x":82,"y":82,"width":83,"height":84,"fill":85},[87,681,683],{"x":89,"y":682,"fill":91,"style":92},"24","Connections demanded: 4 workers x (pool 5 + overflow 5)",[173,685],{"x1":106,"y1":127,"x2":106,"y2":686,"stroke":188,"style":102},"288",[173,688],{"x1":106,"y1":686,"x2":689,"y2":686,"stroke":188,"style":102},"710",[173,691],{"x1":106,"y1":692,"x2":689,"y2":692,"stroke":188,"style":693},"233","stroke-width:1;stroke-dasharray:4 4;",[173,695],{"x1":106,"y1":696,"x2":689,"y2":696,"stroke":188,"style":693},"178",[173,698],{"x1":106,"y1":699,"x2":689,"y2":699,"stroke":188,"style":693},"123",[173,701],{"x1":106,"y1":702,"x2":689,"y2":702,"stroke":188,"style":693},"68",[87,704,82],{"x":176,"y":705,"fill":116,"style":706},"292","text-anchor:end;font-size:11;font-family:var(--font-sans);",[87,708,710],{"x":176,"y":709,"fill":116,"style":706},"237","45",[87,712,714],{"x":176,"y":713,"fill":116,"style":706},"182","90",[87,716,718],{"x":176,"y":717,"fill":116,"style":706},"127","135",[87,720,722],{"x":176,"y":721,"fill":116,"style":706},"72","180",[87,724,726],{"x":129,"y":696,"fill":116,"transform":725,"style":107},"rotate(-90 34 178)","connections",[80,728],{"x":729,"y":730,"width":731,"height":732,"fill":131},"140","239","70","49",[87,734,736],{"x":735,"y":192,"fill":91,"style":117},"175","40",[80,738],{"x":686,"y":739,"width":731,"height":740,"fill":131},"190","98",[87,742,745],{"x":743,"y":744,"fill":91,"style":117},"323","183","80",[80,747],{"x":748,"y":749,"width":731,"height":750,"fill":202},"436","141","147",[87,752,170],{"x":753,"y":754,"fill":91,"style":117},"471","134",[80,756],{"x":757,"y":98,"width":731,"height":758,"fill":202},"584","196",[87,760,763],{"x":761,"y":762,"fill":91,"style":117},"619","85","160",[173,765],{"x1":106,"y1":713,"x2":689,"y2":713,"stroke":101,"style":766},"stroke-width:2;stroke-dasharray:8 4;",[87,768,770],{"x":769,"y":162,"fill":91,"style":706},"706","app budget 87 of max_connections 100",[87,772,773],{"x":735,"y":216,"fill":91,"style":107},"1 container",[87,775,776],{"x":743,"y":216,"fill":91,"style":107},"2 containers",[87,778,779],{"x":753,"y":216,"fill":91,"style":107},"3 containers",[87,781,782],{"x":761,"y":216,"fill":91,"style":107},"4 containers",[87,784,786],{"x":89,"y":785,"fill":116,"style":117},"338","Autoscaling past 2 containers exceeds the budget: shrink the pool, do not raise it",[14,788,789],{},"So compute the pool from the fleet, not from a blog post default. Reserve headroom for migrations, your own psql sessions, and whatever monitoring agent your host attaches, then divide what is left by the number of processes you will ever run at peak.",[226,791,793],{"className":228,"code":792,"language":230,"meta":231,"style":231},"import os\n\ndef pool_dimensions() -> tuple[int, int]:\n    \"\"\"Split the database's usable connections across every worker process.\"\"\"\n    max_conns = int(os.getenv(\"DB_MAX_CONNECTIONS\", \"100\"))\n    reserved = int(os.getenv(\"DB_RESERVED_CONNECTIONS\", \"13\"))  # superuser + ops + migrations\n    containers = int(os.getenv(\"MAX_CONTAINERS\", \"3\"))\n    workers = int(os.getenv(\"WEB_CONCURRENCY\", \"4\"))\n\n    budget = max(max_conns - reserved, 1)\n    per_process = max(budget \u002F\u002F (containers * workers), 2)\n    pool_size = max(per_process * 2 \u002F\u002F 3, 1)\n    return pool_size, per_process - pool_size\n\nPOOL_SIZE, MAX_OVERFLOW = pool_dimensions()\n",[17,794,795,801,805,823,828,850,876,897,918,922,945,974,1003,1015,1019],{"__ignoreMap":231},[235,796,797,799],{"class":173,"line":237},[235,798,241],{"class":240},[235,800,245],{"class":244},[235,802,803],{"class":173,"line":248},[235,804,287],{"emptyLinePlaceholder":286},[235,806,807,809,812,815,817,819,821],{"class":173,"line":256},[235,808,361],{"class":240},[235,810,811],{"class":345}," pool_dimensions",[235,813,814],{"class":244},"() -> tuple[",[235,816,553],{"class":312},[235,818,328],{"class":244},[235,820,553],{"class":312},[235,822,556],{"class":244},[235,824,825],{"class":173,"line":270},[235,826,827],{"class":302},"    \"\"\"Split the database's usable connections across every worker process.\"\"\"\n",[235,829,830,833,835,838,840,843,845,848],{"class":173,"line":283},[235,831,832],{"class":244},"    max_conns ",[235,834,296],{"class":240},[235,836,837],{"class":312}," int",[235,839,322],{"class":244},[235,841,842],{"class":302},"\"DB_MAX_CONNECTIONS\"",[235,844,328],{"class":244},[235,846,847],{"class":302},"\"100\"",[235,849,334],{"class":244},[235,851,852,855,857,859,861,864,866,869,872],{"class":173,"line":290},[235,853,854],{"class":244},"    reserved ",[235,856,296],{"class":240},[235,858,837],{"class":312},[235,860,322],{"class":244},[235,862,863],{"class":302},"\"DB_RESERVED_CONNECTIONS\"",[235,865,328],{"class":244},[235,867,868],{"class":302},"\"13\"",[235,870,871],{"class":244},"))  ",[235,873,875],{"class":874},"sJ8bj","# superuser + ops + migrations\n",[235,877,878,881,883,885,887,890,892,895],{"class":173,"line":309},[235,879,880],{"class":244},"    containers ",[235,882,296],{"class":240},[235,884,837],{"class":312},[235,886,322],{"class":244},[235,888,889],{"class":302},"\"MAX_CONTAINERS\"",[235,891,328],{"class":244},[235,893,894],{"class":302},"\"3\"",[235,896,334],{"class":244},[235,898,899,902,904,906,908,911,913,916],{"class":173,"line":337},[235,900,901],{"class":244},"    workers ",[235,903,296],{"class":240},[235,905,837],{"class":312},[235,907,322],{"class":244},[235,909,910],{"class":302},"\"WEB_CONCURRENCY\"",[235,912,328],{"class":244},[235,914,915],{"class":302},"\"4\"",[235,917,334],{"class":244},[235,919,920],{"class":173,"line":342},[235,921,287],{"emptyLinePlaceholder":286},[235,923,924,927,929,932,935,937,940,943],{"class":173,"line":358},[235,925,926],{"class":244},"    budget ",[235,928,296],{"class":240},[235,930,931],{"class":312}," max",[235,933,934],{"class":244},"(max_conns ",[235,936,478],{"class":240},[235,938,939],{"class":244}," reserved, ",[235,941,942],{"class":312},"1",[235,944,355],{"class":244},[235,946,947,950,952,954,957,960,963,966,969,972],{"class":173,"line":376},[235,948,949],{"class":244},"    per_process ",[235,951,296],{"class":240},[235,953,931],{"class":312},[235,955,956],{"class":244},"(budget ",[235,958,959],{"class":240},"\u002F\u002F",[235,961,962],{"class":244}," (containers ",[235,964,965],{"class":240},"*",[235,967,968],{"class":244}," workers), ",[235,970,971],{"class":312},"2",[235,973,355],{"class":244},[235,975,976,979,981,983,986,988,991,994,997,999,1001],{"class":173,"line":393},[235,977,978],{"class":244},"    pool_size ",[235,980,296],{"class":240},[235,982,931],{"class":312},[235,984,985],{"class":244},"(per_process ",[235,987,965],{"class":240},[235,989,990],{"class":312}," 2",[235,992,993],{"class":240}," \u002F\u002F",[235,995,996],{"class":312}," 3",[235,998,328],{"class":244},[235,1000,942],{"class":312},[235,1002,355],{"class":244},[235,1004,1005,1007,1010,1012],{"class":173,"line":398},[235,1006,579],{"class":240},[235,1008,1009],{"class":244}," pool_size, per_process ",[235,1011,478],{"class":240},[235,1013,1014],{"class":244}," pool_size\n",[235,1016,1017],{"class":173,"line":410},[235,1018,287],{"emptyLinePlaceholder":286},[235,1020,1021,1024,1026,1029,1031],{"class":173,"line":425},[235,1022,1023],{"class":312},"POOL_SIZE",[235,1025,328],{"class":244},[235,1027,1028],{"class":312},"MAX_OVERFLOW",[235,1030,316],{"class":240},[235,1032,1033],{"class":244}," pool_dimensions()\n",[14,1035,1036,1037,1039,1040,1044],{},"With ",[17,1038,658],{}," at 100, thirteen reserved, three containers and four workers, that yields two persistent connections and one overflow per process: twelve processes, thirty-six connections at peak. It looks alarmingly small next to the default. It is correct. An async worker multiplexes thousands of concurrent requests over a handful of connections precisely because most of a request is not spent talking to the database — and if your requests genuinely need more than two each, you have a query problem, not a pool problem. The ",[22,1041,1043],{"href":1042},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002Fuvicorn-vs-gunicorn-worker-configuration\u002F","Uvicorn vs Gunicorn worker configuration"," comparison covers the process count that feeds this formula.",[35,1046,1048],{"id":1047},"the-leak-sessions-kept-alive-by-background-work","The leak: sessions kept alive by background work",[14,1050,1051,1052,1055,1056,1059],{},"Here is the cause of most real exhaustion incidents, and it looks completely innocent. A route takes the request-scoped session from a dependency, hands it to a ",[17,1053,1054],{},"BackgroundTasks"," callback or a bare ",[17,1057,1058],{},"asyncio.create_task",", and returns a response. FastAPI runs background tasks after the response is sent — but the dependency's teardown also runs after the response, and now two things race over the same session. Worse, if the task holds the session for eight seconds writing an audit row and calling Stripe, that connection is checked out for eight seconds instead of forty milliseconds. At twenty requests a second, you need 160 connections to sustain that, and you have three.",[61,1061,70,1066,70,1069,70,1072,70,1075,70,1078,70,1084,70,1089,70,1093,70,1098,70,1101,70,1105,70,1109,70,1113,70,1116,70,1118,70,1121,70,1123,70,1127,70,1131,70,1135,70,1139,70,1143,70,1146],{"viewBox":1062,"role":64,"ariaLabelledBy":1063,"xmlns":68,"style":69},"0 0 760 340",[1064,1065],"cpx-leak-t","cpx-leak-d",[72,1067,1068],{"id":1064},"Session hold time with and without a leak",[76,1070,1071],{"id":1065},"Two timelines: a request-scoped session returns its connection in 40 milliseconds, while a session passed to a background task holds it for 8 seconds after the response.",[80,1073],{"x":82,"y":82,"width":83,"height":1074,"fill":85},"340",[87,1076,1077],{"x":89,"y":682,"fill":91,"style":92},"How long one connection stays checked out",[87,1079,1083],{"x":1080,"y":1081,"fill":91,"style":1082},"30","62","font-size:12;font-family:var(--font-sans);","Correct: session ends with the request",[173,1085],{"x1":1080,"y1":1086,"x2":1087,"y2":1086,"stroke":188,"style":1088},"112","730","stroke-width:1;",[80,1090],{"x":1080,"y":1091,"width":97,"height":1092,"rx":130,"fill":85,"stroke":131,"style":102},"76","28",[87,1094,1097],{"x":1095,"y":1096,"fill":91,"style":117},"105","95","query + commit",[80,1099],{"x":1100,"y":1091,"width":170,"height":1092,"rx":130,"fill":100,"stroke":188,"style":102},"186",[87,1102,1104],{"x":1103,"y":1096,"fill":91,"style":117},"246","close, return",[87,1106,1108],{"x":1107,"y":1096,"fill":91,"style":123},"320","slot free after 40 ms",[87,1110,1112],{"x":1080,"y":1111,"fill":91,"style":1082},"162","Leak: the same session is handed to a background task",[173,1114],{"x1":1080,"y1":1115,"x2":1087,"y2":1115,"stroke":188,"style":1088},"212",[80,1117],{"x":1080,"y":175,"width":97,"height":1092,"rx":130,"fill":85,"stroke":131,"style":102},[87,1119,1097],{"x":1095,"y":1120,"fill":91,"style":117},"195",[80,1122],{"x":1100,"y":175,"width":714,"height":1092,"rx":130,"fill":100,"stroke":188,"style":102},[87,1124,1126],{"x":1125,"y":1120,"fill":91,"style":117},"231","response",[80,1128],{"x":1129,"y":175,"width":1130,"height":1092,"rx":130,"fill":85,"stroke":202,"style":102},"282","330",[87,1132,1134],{"x":1133,"y":1120,"fill":91,"style":117},"447","task calls Stripe, writes audit row",[87,1136,1138],{"x":1137,"y":1120,"fill":91,"style":123},"622","free after 8 s",[80,1140],{"x":1080,"y":1141,"width":1142,"height":127,"rx":99,"fill":100,"stroke":188,"style":102},"240","700",[87,1144,1145],{"x":89,"y":191,"fill":91,"style":107},"200x longer hold means 200x the connections for the same traffic",[87,1147,1148],{"x":89,"y":134,"fill":116,"style":117},"20 req\u002Fs x 8 s = 160 slots needed; 20 req\u002Fs x 0.04 s = under 1",[14,1150,1151,1152,1156,1157,1160],{},"The fix is a rule with no exceptions: ",[1153,1154,1155],"strong",{},"a session belongs to the scope that opened it."," Anything running after the response opens its own session from the same ",[17,1158,1159],{},"async_sessionmaker",", uses it inside a context manager, and closes it. The corrected version below also does the slow HTTP call outside the session, so the connection is held only for the write.",[226,1162,1164],{"className":228,"code":1163,"language":230,"meta":231,"style":231},"import os\nimport httpx\nfrom fastapi import BackgroundTasks, Depends, FastAPI\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\nengine = create_async_engine(os.environ[\"DATABASE_URL\"])\nSession = async_sessionmaker(engine, expire_on_commit=False)\napp = FastAPI()\n\nasync def get_session() -> AsyncSession:\n    async with Session() as session:\n        yield session\n\nasync def record_usage(account_id: str, units: int) -> None:\n    \"\"\"Owns its own session. Never accepts one from a request.\"\"\"\n    async with httpx.AsyncClient(timeout=10.0) as client:\n        await client.post(\n            os.environ[\"METERING_WEBHOOK_URL\"],\n            json={\"account_id\": account_id, \"units\": units},\n        )\n    async with Session() as session:          # opened after the slow call\n        async with session.begin():\n            await session.execute(\n                USAGE_INSERT, {\"account_id\": account_id, \"units\": units}\n            )\n\n@app.post(\"\u002Fv1\u002Frender\")\nasync def render(\n    account_id: str,\n    tasks: BackgroundTasks,\n    session: AsyncSession = Depends(get_session),\n) -> dict[str, str]:\n    await session.execute(TOUCH_ACCOUNT, {\"account_id\": account_id})\n    await session.commit()\n    tasks.add_task(record_usage, account_id, 1)   # pass data, never the session\n    return {\"status\": \"queued\"}\n",[17,1165,1166,1172,1179,1191,1202,1206,1218,1239,1249,1253,1267,1284,1292,1296,1322,1327,1352,1360,1371,1392,1397,1413,1423,1431,1448,1453,1457,1469,1481,1491,1496,1507,1521,1540,1548,1562],{"__ignoreMap":231},[235,1167,1168,1170],{"class":173,"line":237},[235,1169,241],{"class":240},[235,1171,245],{"class":244},[235,1173,1174,1176],{"class":173,"line":248},[235,1175,241],{"class":240},[235,1177,1178],{"class":244}," httpx\n",[235,1180,1181,1183,1186,1188],{"class":173,"line":256},[235,1182,259],{"class":240},[235,1184,1185],{"class":244}," fastapi ",[235,1187,241],{"class":240},[235,1189,1190],{"class":244}," BackgroundTasks, Depends, FastAPI\n",[235,1192,1193,1195,1197,1199],{"class":173,"line":270},[235,1194,259],{"class":240},[235,1196,275],{"class":244},[235,1198,241],{"class":240},[235,1200,1201],{"class":244}," AsyncSession, async_sessionmaker, create_async_engine\n",[235,1203,1204],{"class":173,"line":283},[235,1205,287],{"emptyLinePlaceholder":286},[235,1207,1208,1210,1212,1214,1216],{"class":173,"line":290},[235,1209,293],{"class":244},[235,1211,296],{"class":240},[235,1213,299],{"class":244},[235,1215,303],{"class":302},[235,1217,306],{"class":244},[235,1219,1220,1223,1225,1228,1232,1234,1237],{"class":173,"line":309},[235,1221,1222],{"class":244},"Session ",[235,1224,296],{"class":240},[235,1226,1227],{"class":244}," async_sessionmaker(engine, ",[235,1229,1231],{"class":1230},"s4XuR","expire_on_commit",[235,1233,296],{"class":240},[235,1235,1236],{"class":312},"False",[235,1238,355],{"class":244},[235,1240,1241,1244,1246],{"class":173,"line":337},[235,1242,1243],{"class":244},"app ",[235,1245,296],{"class":240},[235,1247,1248],{"class":244}," FastAPI()\n",[235,1250,1251],{"class":173,"line":342},[235,1252,287],{"emptyLinePlaceholder":286},[235,1254,1255,1258,1261,1264],{"class":173,"line":358},[235,1256,1257],{"class":240},"async",[235,1259,1260],{"class":240}," def",[235,1262,1263],{"class":345}," get_session",[235,1265,1266],{"class":244},"() -> AsyncSession:\n",[235,1268,1269,1272,1275,1278,1281],{"class":173,"line":376},[235,1270,1271],{"class":240},"    async",[235,1273,1274],{"class":240}," with",[235,1276,1277],{"class":244}," Session() ",[235,1279,1280],{"class":240},"as",[235,1282,1283],{"class":244}," session:\n",[235,1285,1286,1289],{"class":173,"line":393},[235,1287,1288],{"class":240},"        yield",[235,1290,1291],{"class":244}," session\n",[235,1293,1294],{"class":173,"line":398},[235,1295,287],{"emptyLinePlaceholder":286},[235,1297,1298,1300,1302,1305,1308,1310,1313,1315,1318,1320],{"class":173,"line":410},[235,1299,1257],{"class":240},[235,1301,1260],{"class":240},[235,1303,1304],{"class":345}," record_usage",[235,1306,1307],{"class":244},"(account_id: ",[235,1309,548],{"class":312},[235,1311,1312],{"class":244},", units: ",[235,1314,553],{"class":312},[235,1316,1317],{"class":244},") -> ",[235,1319,370],{"class":312},[235,1321,373],{"class":244},[235,1323,1324],{"class":173,"line":425},[235,1325,1326],{"class":302},"    \"\"\"Owns its own session. Never accepts one from a request.\"\"\"\n",[235,1328,1329,1331,1333,1336,1339,1341,1344,1347,1349],{"class":173,"line":444},[235,1330,1271],{"class":240},[235,1332,1274],{"class":240},[235,1334,1335],{"class":244}," httpx.AsyncClient(",[235,1337,1338],{"class":1230},"timeout",[235,1340,296],{"class":240},[235,1342,1343],{"class":312},"10.0",[235,1345,1346],{"class":244},") ",[235,1348,1280],{"class":240},[235,1350,1351],{"class":244}," client:\n",[235,1353,1354,1357],{"class":173,"line":461},[235,1355,1356],{"class":240},"        await",[235,1358,1359],{"class":244}," client.post(\n",[235,1361,1362,1365,1368],{"class":173,"line":467},[235,1363,1364],{"class":244},"            os.environ[",[235,1366,1367],{"class":302},"\"METERING_WEBHOOK_URL\"",[235,1369,1370],{"class":244},"],\n",[235,1372,1373,1376,1378,1380,1383,1386,1389],{"class":173,"line":484},[235,1374,1375],{"class":1230},"            json",[235,1377,296],{"class":240},[235,1379,515],{"class":244},[235,1381,1382],{"class":302},"\"account_id\"",[235,1384,1385],{"class":244},": account_id, ",[235,1387,1388],{"class":302},"\"units\"",[235,1390,1391],{"class":244},": units},\n",[235,1393,1394],{"class":173,"line":500},[235,1395,1396],{"class":244},"        )\n",[235,1398,1399,1401,1403,1405,1407,1410],{"class":173,"line":532},[235,1400,1271],{"class":240},[235,1402,1274],{"class":240},[235,1404,1277],{"class":244},[235,1406,1280],{"class":240},[235,1408,1409],{"class":244}," session:          ",[235,1411,1412],{"class":874},"# opened after the slow call\n",[235,1414,1415,1418,1420],{"class":173,"line":537},[235,1416,1417],{"class":240},"        async",[235,1419,1274],{"class":240},[235,1421,1422],{"class":244}," session.begin():\n",[235,1424,1425,1428],{"class":173,"line":559},[235,1426,1427],{"class":240},"            await",[235,1429,1430],{"class":244}," session.execute(\n",[235,1432,1433,1436,1439,1441,1443,1445],{"class":173,"line":565},[235,1434,1435],{"class":312},"                USAGE_INSERT",[235,1437,1438],{"class":244},", {",[235,1440,1382],{"class":302},[235,1442,1385],{"class":244},[235,1444,1388],{"class":302},[235,1446,1447],{"class":244},": units}\n",[235,1449,1450],{"class":173,"line":576},[235,1451,1452],{"class":244},"            )\n",[235,1454,1455],{"class":173,"line":585},[235,1456,287],{"emptyLinePlaceholder":286},[235,1458,1459,1462,1464,1467],{"class":173,"line":594},[235,1460,1461],{"class":345},"@app.post",[235,1463,506],{"class":244},[235,1465,1466],{"class":302},"\"\u002Fv1\u002Frender\"",[235,1468,355],{"class":244},[235,1470,1471,1473,1475,1478],{"class":173,"line":603},[235,1472,1257],{"class":240},[235,1474,1260],{"class":240},[235,1476,1477],{"class":345}," render",[235,1479,1480],{"class":244},"(\n",[235,1482,1483,1486,1488],{"class":173,"line":612},[235,1484,1485],{"class":244},"    account_id: ",[235,1487,548],{"class":312},[235,1489,1490],{"class":244},",\n",[235,1492,1493],{"class":173,"line":626},[235,1494,1495],{"class":244},"    tasks: BackgroundTasks,\n",[235,1497,1499,1502,1504],{"class":173,"line":1498},31,[235,1500,1501],{"class":244},"    session: AsyncSession ",[235,1503,296],{"class":240},[235,1505,1506],{"class":244}," Depends(get_session),\n",[235,1508,1510,1513,1515,1517,1519],{"class":173,"line":1509},32,[235,1511,1512],{"class":244},") -> dict[",[235,1514,548],{"class":312},[235,1516,328],{"class":244},[235,1518,548],{"class":312},[235,1520,556],{"class":244},[235,1522,1524,1527,1530,1533,1535,1537],{"class":173,"line":1523},33,[235,1525,1526],{"class":240},"    await",[235,1528,1529],{"class":244}," session.execute(",[235,1531,1532],{"class":312},"TOUCH_ACCOUNT",[235,1534,1438],{"class":244},[235,1536,1382],{"class":302},[235,1538,1539],{"class":244},": account_id})\n",[235,1541,1543,1545],{"class":173,"line":1542},34,[235,1544,1526],{"class":240},[235,1546,1547],{"class":244}," session.commit()\n",[235,1549,1551,1554,1556,1559],{"class":173,"line":1550},35,[235,1552,1553],{"class":244},"    tasks.add_task(record_usage, account_id, ",[235,1555,942],{"class":312},[235,1557,1558],{"class":244},")   ",[235,1560,1561],{"class":874},"# pass data, never the session\n",[235,1563,1565,1567,1570,1573,1576,1579],{"class":173,"line":1564},36,[235,1566,579],{"class":240},[235,1568,1569],{"class":244}," {",[235,1571,1572],{"class":302},"\"status\"",[235,1574,1575],{"class":244},": ",[235,1577,1578],{"class":302},"\"queued\"",[235,1580,1581],{"class":244},"}\n",[14,1583,1584,1585,1588,1589,1592,1593,1596,1597,1600,1601,1604,1605,1608,1609,1613],{},"Three more leaks worth grepping for. A ",[17,1586,1587],{},"Depends"," that returns a session without ",[17,1590,1591],{},"async with"," or a ",[17,1594,1595],{},"finally"," block never closes it on an exception path. A module-level ",[17,1598,1599],{},"session = Session()"," shared across requests holds one connection forever and corrupts state between users. And any synchronous call inside an async route — a blocking SDK, ",[17,1602,1603],{},"time.sleep",", a ",[17,1606,1607],{},"requests"," call — pins the event loop while every checked-out connection sits idle. If the background work is heavier than a single insert, stop using in-process tasks entirely and move to ",[22,1610,1612],{"href":1611},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","running background jobs with Celery",", where the worker has its own pool and its own failure budget.",[35,1615,1617],{"id":1616},"timeouts-that-fail-fast-instead-of-hanging","Timeouts that fail fast instead of hanging",[14,1619,1620,1621,1623,1624,1627,1628,1631],{},"The default ",[17,1622,55],{}," of thirty seconds is hostile in production. It converts a capacity problem into thirty seconds of held sockets, held memory, and a client that has already given up and retried twice — tripling the load that caused the problem. Set it to five seconds and return a 503 with ",[17,1625,1626],{},"Retry-After",". Customers get a clear signal, retry storms shrink, and your load balancer stops routing to a container that cannot serve. Pair it with a server-side statement timeout so one accidental sequential scan cannot hold a slot for minutes, and a ",[17,1629,1630],{},"pool_recycle"," below whatever idle cutoff your host enforces.",[61,1633,70,1638,70,1641,70,1644,70,1647,70,1654,70,1658,70,1663,70,1666,70,1669,70,1671,70,1675,70,1679,70,1681,70,1684,70,1687,70,1690,70,1694,70,1697,70,1700,70,1702,70,1704,70,1706,70,1709,70,1713,70,1717,70,1719,70,1722,70,1725,70,1728,70,1730,70,1733,70,1736,70,1739],{"viewBox":1634,"role":64,"ariaLabelledBy":1635,"xmlns":68,"style":69},"0 0 760 350",[1636,1637],"cpx-tree-t","cpx-tree-d",[72,1639,1640],{"id":1636},"Which cause of pool exhaustion is this",[76,1642,1643],{"id":1637},"A decision tree comparing checked-out connections with active database queries to distinguish a session leak, slow queries, and a genuinely undersized pool.",[80,1645],{"x":82,"y":82,"width":83,"height":1646,"fill":85},"350",[80,1648],{"x":1649,"y":1650,"width":1651,"height":1652,"rx":1653,"fill":100,"stroke":101,"style":102},"245","20","270","46","10",[87,1655,1657],{"x":89,"y":732,"fill":91,"style":1656},"text-anchor:middle;font-size:13;font-family:var(--font-sans);","Pool at 100% — compare counters",[1659,1660],"polyline",{"points":1661,"fill":1662,"stroke":188,"style":102},"380,66 380,92 140,92 140,120","none",[1659,1664],{"points":1665,"fill":1662,"stroke":188,"style":102},"380,92 380,120",[1659,1667],{"points":1668,"fill":1662,"stroke":188,"style":102},"380,92 620,92 620,120",[80,1670],{"x":682,"y":170,"width":192,"height":731,"rx":99,"fill":85,"stroke":202,"style":102},[87,1672,1674],{"x":729,"y":1673,"fill":91,"style":107},"146","DB shows 2 active queries",[87,1676,1678],{"x":729,"y":1677,"fill":91,"style":107},"168","but 10 slots checked out",[80,1680],{"x":191,"y":170,"width":192,"height":731,"rx":99,"fill":85,"stroke":150,"style":102},[87,1682,1683],{"x":89,"y":1673,"fill":91,"style":107},"DB shows 10 active queries",[87,1685,1686],{"x":89,"y":1677,"fill":91,"style":107},"each running over 1 s",[80,1688],{"x":1689,"y":170,"width":192,"height":731,"rx":99,"fill":85,"stroke":131,"style":102},"504",[87,1691,1693],{"x":1692,"y":1673,"fill":91,"style":107},"620","Queries fast, holds short,",[87,1695,1696],{"x":1692,"y":1677,"fill":91,"style":107},"traffic simply doubled",[173,1698],{"x1":729,"y1":739,"x2":729,"y2":1699,"stroke":188,"style":102},"214",[173,1701],{"x1":89,"y1":739,"x2":89,"y2":1699,"stroke":188,"style":102},[173,1703],{"x1":1692,"y1":739,"x2":1692,"y2":1699,"stroke":188,"style":102},[80,1705],{"x":682,"y":1699,"width":192,"height":1091,"rx":99,"fill":100,"stroke":188,"style":102},[87,1707,1708],{"x":729,"y":1141,"fill":91,"style":107},"Session leak",[87,1710,1712],{"x":729,"y":1711,"fill":116,"style":117},"262","background task or missing",[87,1714,1716],{"x":729,"y":1715,"fill":116,"style":117},"278","close on the error path",[80,1718],{"x":191,"y":1699,"width":192,"height":1091,"rx":99,"fill":100,"stroke":188,"style":102},[87,1720,1721],{"x":89,"y":1141,"fill":91,"style":107},"Slow queries",[87,1723,1724],{"x":89,"y":1711,"fill":116,"style":117},"add an index, cap statement",[87,1726,1727],{"x":89,"y":1715,"fill":116,"style":117},"timeout, cache the read",[80,1729],{"x":1689,"y":1699,"width":192,"height":1091,"rx":99,"fill":100,"stroke":188,"style":102},[87,1731,1732],{"x":1692,"y":1141,"fill":91,"style":107},"Genuinely undersized",[87,1734,1735],{"x":1692,"y":1711,"fill":116,"style":117},"raise the pool only if the",[87,1737,1738],{"x":1692,"y":1715,"fill":116,"style":117},"server budget allows it",[87,1740,1742],{"x":89,"y":1741,"fill":116,"style":117},"326","Only the right-hand branch justifies a bigger pool or a pooler",[226,1744,1746],{"className":228,"code":1745,"language":230,"meta":231,"style":231},"import os\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nSTATEMENT_TIMEOUT_MS = os.getenv(\"DB_STATEMENT_TIMEOUT_MS\", \"5000\")\n\nengine = create_async_engine(\n    os.environ[\"DATABASE_URL\"],\n    pool_size=POOL_SIZE,\n    max_overflow=MAX_OVERFLOW,\n    pool_timeout=float(os.getenv(\"DB_POOL_TIMEOUT\", \"5\")),\n    pool_recycle=int(os.getenv(\"DB_POOL_RECYCLE\", \"1800\")),\n    pool_pre_ping=True,\n    connect_args={\n        \"timeout\": float(os.getenv(\"DB_CONNECT_TIMEOUT\", \"5\")),\n        \"server_settings\": {\n            \"statement_timeout\": STATEMENT_TIMEOUT_MS,\n            \"idle_in_transaction_session_timeout\": os.getenv(\"DB_IDLE_TXN_MS\", \"10000\"),\n        },\n    },\n)\n",[17,1747,1748,1754,1764,1768,1788,1792,1801,1810,1821,1832,1855,1876,1888,1898,1918,1926,1937,1956,1961,1966],{"__ignoreMap":231},[235,1749,1750,1752],{"class":173,"line":237},[235,1751,241],{"class":240},[235,1753,245],{"class":244},[235,1755,1756,1758,1760,1762],{"class":173,"line":248},[235,1757,259],{"class":240},[235,1759,275],{"class":244},[235,1761,241],{"class":240},[235,1763,280],{"class":244},[235,1765,1766],{"class":173,"line":256},[235,1767,287],{"emptyLinePlaceholder":286},[235,1769,1770,1773,1775,1778,1781,1783,1786],{"class":173,"line":270},[235,1771,1772],{"class":312},"STATEMENT_TIMEOUT_MS",[235,1774,316],{"class":240},[235,1776,1777],{"class":244}," os.getenv(",[235,1779,1780],{"class":302},"\"DB_STATEMENT_TIMEOUT_MS\"",[235,1782,328],{"class":244},[235,1784,1785],{"class":302},"\"5000\"",[235,1787,355],{"class":244},[235,1789,1790],{"class":173,"line":283},[235,1791,287],{"emptyLinePlaceholder":286},[235,1793,1794,1796,1798],{"class":173,"line":290},[235,1795,293],{"class":244},[235,1797,296],{"class":240},[235,1799,1800],{"class":244}," create_async_engine(\n",[235,1802,1803,1806,1808],{"class":173,"line":309},[235,1804,1805],{"class":244},"    os.environ[",[235,1807,303],{"class":302},[235,1809,1370],{"class":244},[235,1811,1812,1815,1817,1819],{"class":173,"line":337},[235,1813,1814],{"class":1230},"    pool_size",[235,1816,296],{"class":240},[235,1818,1023],{"class":312},[235,1820,1490],{"class":244},[235,1822,1823,1826,1828,1830],{"class":173,"line":342},[235,1824,1825],{"class":1230},"    max_overflow",[235,1827,296],{"class":240},[235,1829,1028],{"class":312},[235,1831,1490],{"class":244},[235,1833,1834,1837,1839,1842,1844,1847,1849,1852],{"class":173,"line":358},[235,1835,1836],{"class":1230},"    pool_timeout",[235,1838,296],{"class":240},[235,1840,1841],{"class":312},"float",[235,1843,322],{"class":244},[235,1845,1846],{"class":302},"\"DB_POOL_TIMEOUT\"",[235,1848,328],{"class":244},[235,1850,1851],{"class":302},"\"5\"",[235,1853,1854],{"class":244},")),\n",[235,1856,1857,1860,1862,1864,1866,1869,1871,1874],{"class":173,"line":376},[235,1858,1859],{"class":1230},"    pool_recycle",[235,1861,296],{"class":240},[235,1863,553],{"class":312},[235,1865,322],{"class":244},[235,1867,1868],{"class":302},"\"DB_POOL_RECYCLE\"",[235,1870,328],{"class":244},[235,1872,1873],{"class":302},"\"1800\"",[235,1875,1854],{"class":244},[235,1877,1878,1881,1883,1886],{"class":173,"line":393},[235,1879,1880],{"class":1230},"    pool_pre_ping",[235,1882,296],{"class":240},[235,1884,1885],{"class":312},"True",[235,1887,1490],{"class":244},[235,1889,1890,1893,1895],{"class":173,"line":398},[235,1891,1892],{"class":1230},"    connect_args",[235,1894,296],{"class":240},[235,1896,1897],{"class":244},"{\n",[235,1899,1900,1903,1905,1907,1909,1912,1914,1916],{"class":173,"line":410},[235,1901,1902],{"class":302},"        \"timeout\"",[235,1904,1575],{"class":244},[235,1906,1841],{"class":312},[235,1908,322],{"class":244},[235,1910,1911],{"class":302},"\"DB_CONNECT_TIMEOUT\"",[235,1913,328],{"class":244},[235,1915,1851],{"class":302},[235,1917,1854],{"class":244},[235,1919,1920,1923],{"class":173,"line":425},[235,1921,1922],{"class":302},"        \"server_settings\"",[235,1924,1925],{"class":244},": {\n",[235,1927,1928,1931,1933,1935],{"class":173,"line":444},[235,1929,1930],{"class":302},"            \"statement_timeout\"",[235,1932,1575],{"class":244},[235,1934,1772],{"class":312},[235,1936,1490],{"class":244},[235,1938,1939,1942,1945,1948,1950,1953],{"class":173,"line":461},[235,1940,1941],{"class":302},"            \"idle_in_transaction_session_timeout\"",[235,1943,1944],{"class":244},": os.getenv(",[235,1946,1947],{"class":302},"\"DB_IDLE_TXN_MS\"",[235,1949,328],{"class":244},[235,1951,1952],{"class":302},"\"10000\"",[235,1954,1955],{"class":244},"),\n",[235,1957,1958],{"class":173,"line":467},[235,1959,1960],{"class":244},"        },\n",[235,1962,1963],{"class":173,"line":484},[235,1964,1965],{"class":244},"    },\n",[235,1967,1968],{"class":173,"line":500},[235,1969,355],{"class":244},[14,1971,1972,1975,1976,1979,1980,1984],{},[17,1973,1974],{},"idle_in_transaction_session_timeout"," is the underrated one. It kills any connection that opened a transaction and then went to sleep — the exact state a leaked session leaves behind — so the database reclaims what your process forgot. The ",[17,1977,1978],{},"connect_args"," shown target asyncpg; psycopg 3 spells them differently, which the ",[22,1981,1983],{"href":1982},"\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Fasyncpg-vs-psycopg3-for-fastapi\u002F","asyncpg vs psycopg3 comparison"," sets out.",[14,1986,1987],{},"Finally, translate the exception into a proper HTTP answer rather than a generic 500:",[226,1989,1991],{"className":228,"code":1990,"language":230,"meta":231,"style":231},"from fastapi import Request\nfrom fastapi.responses import JSONResponse\nfrom sqlalchemy.exc import TimeoutError as PoolTimeout\n\n@app.exception_handler(PoolTimeout)\nasync def pool_timeout_handler(request: Request, exc: PoolTimeout) -> JSONResponse:\n    return JSONResponse(\n        status_code=503,\n        content={\"error\": \"database_busy\", \"retry_after\": 2},\n        headers={\"Retry-After\": \"2\"},\n    )\n",[17,1992,1993,2004,2016,2034,2038,2046,2058,2065,2077,2106,2125],{"__ignoreMap":231},[235,1994,1995,1997,1999,2001],{"class":173,"line":237},[235,1996,259],{"class":240},[235,1998,1185],{"class":244},[235,2000,241],{"class":240},[235,2002,2003],{"class":244}," Request\n",[235,2005,2006,2008,2011,2013],{"class":173,"line":248},[235,2007,259],{"class":240},[235,2009,2010],{"class":244}," fastapi.responses ",[235,2012,241],{"class":240},[235,2014,2015],{"class":244}," JSONResponse\n",[235,2017,2018,2020,2023,2025,2028,2031],{"class":173,"line":256},[235,2019,259],{"class":240},[235,2021,2022],{"class":244}," sqlalchemy.exc ",[235,2024,241],{"class":240},[235,2026,2027],{"class":312}," TimeoutError",[235,2029,2030],{"class":240}," as",[235,2032,2033],{"class":244}," PoolTimeout\n",[235,2035,2036],{"class":173,"line":270},[235,2037,287],{"emptyLinePlaceholder":286},[235,2039,2040,2043],{"class":173,"line":283},[235,2041,2042],{"class":345},"@app.exception_handler",[235,2044,2045],{"class":244},"(PoolTimeout)\n",[235,2047,2048,2050,2052,2055],{"class":173,"line":290},[235,2049,1257],{"class":240},[235,2051,1260],{"class":240},[235,2053,2054],{"class":345}," pool_timeout_handler",[235,2056,2057],{"class":244},"(request: Request, exc: PoolTimeout) -> JSONResponse:\n",[235,2059,2060,2062],{"class":173,"line":309},[235,2061,579],{"class":240},[235,2063,2064],{"class":244}," JSONResponse(\n",[235,2066,2067,2070,2072,2075],{"class":173,"line":337},[235,2068,2069],{"class":1230},"        status_code",[235,2071,296],{"class":240},[235,2073,2074],{"class":312},"503",[235,2076,1490],{"class":244},[235,2078,2079,2082,2084,2086,2089,2091,2094,2096,2099,2101,2103],{"class":173,"line":342},[235,2080,2081],{"class":1230},"        content",[235,2083,296],{"class":240},[235,2085,515],{"class":244},[235,2087,2088],{"class":302},"\"error\"",[235,2090,1575],{"class":244},[235,2092,2093],{"class":302},"\"database_busy\"",[235,2095,328],{"class":244},[235,2097,2098],{"class":302},"\"retry_after\"",[235,2100,1575],{"class":244},[235,2102,971],{"class":312},[235,2104,2105],{"class":244},"},\n",[235,2107,2108,2111,2113,2115,2118,2120,2123],{"class":173,"line":358},[235,2109,2110],{"class":1230},"        headers",[235,2112,296],{"class":240},[235,2114,515],{"class":244},[235,2116,2117],{"class":302},"\"Retry-After\"",[235,2119,1575],{"class":244},[235,2121,2122],{"class":302},"\"2\"",[235,2124,2105],{"class":244},[235,2126,2127],{"class":173,"line":376},[235,2128,2129],{"class":244},"    )\n",[35,2131,2133],{"id":2132},"when-to-raise-the-pool-and-when-to-refuse","When to raise the pool, and when to refuse",[14,2135,2136,2137,2139],{},"Raise ",[17,2138,32],{}," only when three things are true at once: holds are short, the database shows the same number of active queries as your checked-out count, and the fleet total still fits the server budget. That is the right-hand branch of the tree above and it is the rarest of the three.",[2141,2142,2143,2159],"table",{},[2144,2145,2146],"thead",{},[2147,2148,2149,2153,2156],"tr",{},[2150,2151,2152],"th",{},"Symptom",[2150,2154,2155],{},"Real cause",[2150,2157,2158],{},"Fix",[2160,2161,2162,2174,2185,2196],"tbody",{},[2147,2163,2164,2168,2171],{},[2165,2166,2167],"td",{},"Holds over 2s, DB idle",[2165,2169,2170],{},"Leaked session",[2165,2172,2173],{},"Own session per task",[2147,2175,2176,2179,2182],{},[2165,2177,2178],{},"Holds match slow queries",[2165,2180,2181],{},"Missing index",[2165,2183,2184],{},"Index or cache",[2147,2186,2187,2190,2193],{},[2165,2188,2189],{},"Fails only when scaled out",[2165,2191,2192],{},"Fleet exceeds cap",[2165,2194,2195],{},"Shrink pool, add pooler",[2147,2197,2198,2201,2204],{},[2165,2199,2200],{},"Fails after every deploy",[2165,2202,2203],{},"Old containers linger",[2165,2205,2206],{},"Drain before cutover",[14,2208,2209,2210,2214],{},"Below roughly fifty requests per second, a correctly written async API on a small managed Postgres does not need a pooler at all — two or three connections per process is genuinely enough, and adding PgBouncer buys you an extra hop and a transaction-mode compatibility problem for nothing. Above that, or the moment autoscaling can multiply your process count, put a pooler in front and set the application pool small. Deploy-time failures are a different animal: if old containers keep their connections while new ones open theirs, you briefly need double, which is why ",[22,2211,2213],{"href":2212},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002F","zero-downtime deploys"," need a drain step, not just a health check.",[35,2216,2218],{"id":2217},"the-transition-path-in-order","The transition path, in order",[14,2220,2221],{},"Do these in sequence, and stop as soon as the graph flattens — most teams never reach step five.",[2223,2224,2225,2233,2239,2254,2266,2274],"ol",{},[2226,2227,2228,2229,2232],"li",{},"Ship the checkout instrumentation and the ",[17,2230,2231],{},"pool_stats"," route. Do not change any setting yet. Watch for one traffic peak.",[2226,2234,2235,2236,2238],{},"Cut ",[17,2237,55],{}," to five seconds and add the 503 handler. This makes failures fast and visible instead of silently doubling your latency.",[2226,2240,2241,2242,2245,2246,2249,2250,2253],{},"Grep for every ",[17,2243,2244],{},"Session("," and ",[17,2247,2248],{},"Depends(get_session)"," that crosses a response boundary or a ",[17,2251,2252],{},"create_task",". Fix them to own their session. This is usually the whole incident.",[2226,2255,2256,2257,2245,2260,2262,2263,2265],{},"Apply the per-process formula and set ",[17,2258,2259],{},"statement_timeout",[17,2261,1974],{},". Verify the fleet total against ",[17,2264,658],{}," explicitly.",[2226,2267,2268,2269,2273],{},"Only now, if reads dominate, add a cache in front of the hottest queries — ",[22,2270,2272],{"href":2271},"\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002F","caching API responses with Redis"," removes more database load than any pool tuning will.",[2226,2275,2276],{},"If you are still at the limit, add a transaction-mode pooler and shrink the application pool further.",[14,2278,2279,2280,2283,2284,2288],{},"Cover the leak with a test so it never returns. Fire fifty concurrent requests against a pool of two, assert every response is a 200 or a 503, then assert ",[17,2281,2282],{},"pool.checkedout()"," returns to zero within a second — ",[22,2285,2287],{"href":2286},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002F","testing Python APIs with pytest"," has the async fixtures for it. That assertion catches a reintroduced leak in CI, which is the only place catching it is cheap.",[35,2290,2292],{"id":2291},"builder-verdict","Builder verdict",[14,2294,2295,2296,2298,2299,2301,2302,2306],{},"Fix the leak, shrink the pool, and cut the timeout. That is the order, and it is nearly always the answer. The instinct to raise ",[17,2297,32],{}," is wrong because it treats a symptom that costs you nothing to observe and buys a failure that costs you a database restart — and every builder who has upgraded a Postgres instance to fix a leak has paid forty dollars a month, permanently, for a bug that a ",[17,2300,1595],{}," block would have closed. Small pools plus fast timeouts also make your capacity legible: when three connections per process saturate, you know exactly what a fourth container buys you, which is the number you need to price a plan properly. Work it through with ",[22,2303,2305],{"href":2304},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request"," once the graph is flat.",[14,2308,2309],{},"Spend the afternoon on instrumentation before you spend a dollar on a bigger database. Checkout timing and a pool-stats endpoint take twenty minutes to write, survive every future incident, and turn the next exhaustion event from a day of guessing into a ten-minute diagnosis.",[35,2311,2313],{"id":2312},"faq","FAQ",[14,2315,2316,2319,2320,2322],{},[1153,2317,2318],{},"Does fixing pool exhaustion save real money, or just latency?","\nBoth, and the money is larger than it looks. The usual reflex is to upgrade the database tier so ",[17,2321,658],{}," doubles, which costs forty to two hundred dollars a month forever. A leaked session fixed in one afternoon removes that spend permanently, and the smaller pool often lets you run fewer containers because requests stop piling up behind the queue.",[14,2324,2325,2328,2329,2331],{},[1153,2326,2327],{},"How many connections should each worker really get?","\nTake your database's ",[17,2330,658],{},", subtract about thirteen for superuser slots, migrations, and monitoring, then divide by the maximum number of worker processes you will ever run at peak. Two to five per process is normal and correct for an async API. If that feels too small, measure your hold times before adding capacity — a well-behaved request holds a connection for under fifty milliseconds.",[14,2333,2334,2337],{},[1153,2335,2336],{},"Is adding PgBouncer worth the migration risk?","\nNot until autoscaling can push your process count past the server budget. It adds a hop, and transaction pooling mode breaks prepared statements unless you configure the driver for it. Below fifty requests per second, shrink the application pool instead: same result, no new component to operate or debug at midnight.",[14,2339,2340,2343,2344,2346],{},[1153,2341,2342],{},"Can I just raise pool_timeout so requests stop failing?","\nNo — that is the worst available change. A longer timeout means clients wait, give up, and retry, multiplying the load that caused the exhaustion. Five seconds plus a 503 with ",[17,2345,1626],{}," sheds load and keeps your health check honest so the platform can route around a sick container.",[14,2348,2349,2352],{},[1153,2350,2351],{},"Will this break customers during the migration to smaller pools?","\nOnly if you shrink the pool before fixing the leak. Deploy in the documented order — instrumentation, timeout, leak fix, then sizing — and each step is independently reversible via environment variables. Roll one container first and watch checked-out counts for an hour before applying the change fleet-wide.",[35,2354,2356],{"id":2355},"related","Related",[14,2358,2359],{},[1153,2360,2361],{},"Same track:",[2363,2364,2365,2370,2376,2382],"ul",{},[2226,2366,2367,2369],{},[22,2368,25],{"href":24}," — the parent guide covering engine lifecycle, per-request transactions, and pool defaults.",[2226,2371,2372,2375],{},[22,2373,2374],{"href":1982},"asyncpg vs psycopg3 for FastAPI"," — driver choice decides how your connect args and pooler compatibility are spelled.",[2226,2377,2378,2381],{},[22,2379,2380],{"href":2271},"Caching Python API Responses with Redis"," — the fastest way to cut database load once the leak is closed.",[2226,2383,2384,2387],{},[22,2385,2386],{"href":1611},"Running Background Jobs with Celery"," — move heavy post-response work out of the request process and off its pool.",[14,2389,2390],{},[1153,2391,2392],{},"Other tracks:",[2363,2394,2395,2401],{},[2226,2396,2397,2400],{},[22,2398,2399],{"href":1042},"Uvicorn vs Gunicorn Worker Configuration"," — the process count that drives the per-worker connection maths.",[2226,2402,2403,2407],{},[22,2404,2406],{"href":2405},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Frender-vs-railway-vs-fly-io\u002F","Render vs Railway vs Fly.io"," — how each host's autoscaling multiplies your pool without telling you.",[2409,2410,2411],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":231,"searchDepth":248,"depth":248,"links":2413},[2414,2415,2416,2417,2418,2419,2420,2421,2422],{"id":37,"depth":248,"text":38},{"id":647,"depth":248,"text":648},{"id":1047,"depth":248,"text":1048},{"id":1616,"depth":248,"text":1617},{"id":2132,"depth":248,"text":2133},{"id":2217,"depth":248,"text":2218},{"id":2291,"depth":248,"text":2292},{"id":2312,"depth":248,"text":2313},{"id":2355,"depth":248,"text":2356},"Diagnose QueuePool limit reached in production: size pools against real Postgres limits, find sessions leaked by background tasks, and set timeouts that fail fast.","md",{"pageTitle":2426,"type":2427,"datePublished":2428,"dateModified":2428},"Fixing Connection Pool Exhaustion in Async SQLAlchemy","article","2026-07-23","\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Ffixing-connection-pool-exhaustion",{"title":5,"description":2423},"scaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Ffixing-connection-pool-exhaustion\u002Findex","LQQqV1VGUl-AwE8ruynxKxtRtcpRK59J5h6vVX-3TtA",{"@context":2434,"@type":2435,"mainEntity":2436},"https:\u002F\u002Fschema.org","FAQPage",[2437,2442,2445,2448,2451],{"@type":2438,"name":2318,"acceptedAnswer":2439},"Question",{"@type":2440,"text":2441},"Answer","Both, and the money is larger than it looks. The usual reflex is to upgrade the database tier so max_connections doubles, which costs forty to two hundred dollars a month forever. A leaked session fixed in one afternoon removes that spend permanently, and the smaller pool often lets you run fewer containers because requests stop piling up behind the queue.",{"@type":2438,"name":2327,"acceptedAnswer":2443},{"@type":2440,"text":2444},"Take your database's max_connections, subtract about thirteen for superuser slots, migrations, and monitoring, then divide by the maximum number of worker processes you will ever run at peak. Two to five per process is normal and correct for an async API. If that feels too small, measure your hold times before adding capacity — a well-behaved request holds a connection for under fifty milliseconds.",{"@type":2438,"name":2336,"acceptedAnswer":2446},{"@type":2440,"text":2447},"Not until autoscaling can push your process count past the server budget. It adds a hop, and transaction pooling mode breaks prepared statements unless you configure the driver for it. Below fifty requests per second, shrink the application pool instead: same result, no new component to operate or debug at midnight.",{"@type":2438,"name":2342,"acceptedAnswer":2449},{"@type":2440,"text":2450},"No — that is the worst available change. A longer timeout means clients wait, give up, and retry, multiplying the load that caused the exhaustion. Five seconds plus a 503 with Retry-After sheds load and keeps your health check honest so the platform can route around a sick container.",{"@type":2438,"name":2351,"acceptedAnswer":2452},{"@type":2440,"text":2453},"Only if you shrink the pool before fixing the leak. Deploy in the documented order — instrumentation, timeout, leak fix, then sizing — and each step is independently reversible via environment variables. Roll one container first and watch checked-out counts for an hour before applying the change fleet-wide.",1784887028458]