[{"data":1,"prerenderedAt":3080},["ShallowReactive",2],{"page-\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002F":3},{"id":4,"title":5,"body":6,"description":3072,"extension":3073,"meta":3074,"navigation":380,"path":3076,"seo":3077,"stem":3078,"__hash__":3079},"content\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002Findex.md","Tuning Connection Pools for Cloud Databases",{"type":7,"value":8,"toc":3042},"minimark",[9,13,46,51,56,83,100,103,113,122,126,154,336,340,344,350,716,725,731,734,825,831,837,938,961,965,976,1220,1223,1227,1252,1533,1549,1553,1557,1564,1567,1594,1745,1763,1767,1770,1927,1933,1937,1943,2222,2235,2239,2244,2383,2387,2399,2403,2407,2410,2444,2450,2461,2465,2474,2791,2795,2890,2894,2913,2930,2948,2971,3003,3007,3038],[10,11,5],"h1",{"id":12},"tuning-connection-pools-for-cloud-databases",[14,15,16,17,21,22,25,26,25,29,25,32,35,36,39,40,45],"p",{},"Managing a SQLAlchemy async connection pool against a managed cloud Postgres service — AWS RDS, Aurora, GCP Cloud SQL, or Azure Database for PostgreSQL — demands more discipline than tuning against a self-hosted database. Cloud providers enforce hard connection limits that vary by instance class, interpose their own proxying infrastructure, and aggressively recycle idle TCP connections. Getting any single parameter wrong ripples into ",[18,19,20],"code",{},"QueuePool limit reached"," errors, silent connection drops, or excess costs from over-provisioning. This guide covers every lever — ",[18,23,24],{},"pool_size",", ",[18,27,28],{},"max_overflow",[18,30,31],{},"pool_recycle",[18,33,34],{},"pool_pre_ping",", and ",[18,37,38],{},"NullPool"," — with concrete numbers and the reasoning behind them. For the architectural foundation of async engine initialization, see ",[41,42,44],"a",{"href":43},"\u002Fasync-engines-dialects-and-connection-pooling\u002F","Async Engines, Dialects, and Connection Pooling",".",[47,48,50],"h2",{"id":49},"concept-execution-model","Concept & Execution Model",[52,53,55],"h3",{"id":54},"how-the-pool-budget-works-against-a-cloud-instance","How the pool budget works against a cloud instance",[14,57,58,59,62,63,66,67,70,71,74,75,78,79,82],{},"Every cloud Postgres instance has a ",[18,60,61],{},"max_connections"," limit. Unlike a bare metal database where you set this yourself in ",[18,64,65],{},"postgresql.conf",", a managed service derives it from the instance class using a formula the provider controls. RDS and Aurora use ",[18,68,69],{},"LEAST(DBInstanceClassMemory\u002F9531392, 5000)"," for most engines; a ",[18,72,73],{},"db.t3.medium"," (2 vCPU, 4 GiB) gets around 170 connections. A ",[18,76,77],{},"db.r6g.large"," (2 vCPU, 16 GiB) gets around 683. GCP Cloud SQL uses approximately ",[18,80,81],{},"25 + 0.75 * (RAM_MB \u002F 4)"," for Postgres. Azure Database for PostgreSQL – Flexible Server has a similar memory-based curve.",[14,84,85,86,92,93,96,97,99],{},"The critical constraint is that the ",[87,88,89,90],"strong",{},"total connections opened across all application instances and workers must never exceed ",[18,91,61],{},", and you must reserve headroom for the superuser (",[18,94,95],{},"superuser_reserved_connections",", default 3), replication slots, Alembic migrations, monitoring agents (PgBouncer stat collectors, CloudWatch agents), and any interactive admin sessions. In practice, reserve at least 10 connections plus 5% of ",[18,98,61],{}," for non-application traffic.",[14,101,102],{},"The connection budget formula:",[104,105,110],"pre",{"className":106,"code":108,"language":109},[107],"language-text","usable_connections = max_connections\n                     - superuser_reserved_connections  (typically 3)\n                     - monitoring_and_admin_reserve    (typically 10)\n                     - replication_slots               (0–5)\n\nconnections_per_instance = floor(usable_connections \u002F app_instance_count)\n\npool_size    = floor(connections_per_instance * 0.7)\nmax_overflow = floor(connections_per_instance * 0.3)\n","text",[18,111,108],{"__ignoreMap":112},"",[14,114,115,116,118,119,121],{},"Using 70 % for ",[18,117,24],{}," and 30 % for ",[18,120,28],{}," gives the pool room to burst during traffic spikes while keeping worst-case usage within the per-instance budget.",[52,123,125],{"id":124},"async-execution-model-and-connection-checkout","Async execution model and connection checkout",[14,127,128,129,132,133,136,137,140,141,144,145,148,149,132,151,153],{},"With ",[18,130,131],{},"create_async_engine"," and ",[18,134,135],{},"asyncpg",", connections are managed by ",[18,138,139],{},"AsyncAdaptedQueuePool",". Coroutines check out a connection when ",[18,142,143],{},"session.begin()"," or ",[18,146,147],{},"engine.connect()"," is entered and return it when the context manager exits. Because async Python is cooperative, a single thread can multiplex many coroutines over a smaller pool than a threaded model would need — but the database still sees one TCP connection per pool slot. The cloud provider's network ACLs and idle-timeout machinery act at the TCP layer, not the SQLAlchemy layer, which is why ",[18,150,31],{},[18,152,34],{}," matter so much in cloud environments.",[155,156,159],"figure",{"className":157},[158],"diagram",[160,161,166,167,166,171,166,166,175,166,166,183,166,190,166,196,166,166,202,166,210,166,214,166,166,218,166,223,166,227,166,231,166,166,235,166,239,166,243,166,246,166,166,249,166,253,166,257,166,260,166,166,166,263,166,269,166,273,166,276,166,280,166,166,284,166,286,166,290,166,293,166,296,166,166,299,166,302,166,306,166,309,166,312,166,166,315,166,323,166,327,166,331],"svg",{"viewBox":162,"role":163,"ariaLabel":164,"xmlns":165},"0 0 770 380","img","Connection pool budget diagram showing app instances, pool_size, max_overflow vs database max_connections","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[168,169,170],"title",{},"Connection pool budget vs DB max_connections",[172,173,174],"desc",{},"Three app instances each hold pool_size + max_overflow connections. The total must stay inside the usable portion of DB max_connections, with reserved headroom for admin and superuser traffic.",[176,177],"rect",{"x":178,"y":178,"width":179,"height":180,"fill":181,"rx":182},"0","770","380","#f1f9f6","8",[176,184],{"x":185,"y":186,"width":187,"height":188,"fill":189,"rx":182},"530","30","160","320","#123a35",[109,191,195],{"x":192,"y":193,"fill":181,"style":194},"610","56","text-anchor:middle;font-size:12px;font-weight:bold","DB max_connections",[109,197,201],{"x":192,"y":198,"fill":199,"style":200},"72","#1f9f95","text-anchor:middle;font-size:11px","170 (db.t3.medium)",[176,203],{"x":204,"y":205,"width":206,"height":207,"fill":208,"rx":209},"540","82","140","34","#3f4f4b","4",[109,211,213],{"x":192,"y":212,"fill":181,"style":200},"100","Reserved (superuser",[109,215,217],{"x":192,"y":216,"fill":181,"style":200},"114","+ admin ≈ 13–15)",[176,219],{"x":204,"y":220,"width":206,"height":221,"fill":222,"rx":209},"122","66","#0f766e",[109,224,226],{"x":192,"y":225,"fill":181,"style":200},"143","Instance 1",[109,228,230],{"x":192,"y":229,"fill":181,"style":200},"158","pool_size 35",[109,232,234],{"x":192,"y":233,"fill":199,"style":200},"173","+ overflow 15",[176,236],{"x":204,"y":237,"width":206,"height":221,"fill":222,"rx":209,"opacity":238},"194","0.85",[109,240,242],{"x":192,"y":241,"fill":181,"style":200},"215","Instance 2",[109,244,230],{"x":192,"y":245,"fill":181,"style":200},"230",[109,247,234],{"x":192,"y":248,"fill":199,"style":200},"245",[176,250],{"x":204,"y":251,"width":206,"height":221,"fill":222,"rx":209,"opacity":252},"266","0.70",[109,254,256],{"x":192,"y":255,"fill":181,"style":200},"287","Instance 3",[109,258,230],{"x":192,"y":259,"fill":181,"style":200},"302",[109,261,234],{"x":192,"y":262,"fill":199,"style":200},"317",[176,264],{"x":186,"y":265,"width":266,"height":267,"fill":222,"rx":268},"50","190","90","6",[109,270,272],{"x":271,"y":198,"fill":181,"style":194},"125","App Instance 1",[109,274,275],{"x":271,"y":267,"fill":181,"style":200},"pool_size=35",[109,277,279],{"x":271,"y":278,"fill":199,"style":200},"106","max_overflow=15",[109,281,283],{"x":271,"y":220,"fill":181,"style":282},"text-anchor:middle;font-size:10px","worst-case: 50 conns",[176,285],{"x":186,"y":187,"width":266,"height":267,"fill":222,"rx":268,"opacity":238},[109,287,289],{"x":271,"y":288,"fill":181,"style":194},"182","App Instance 2",[109,291,275],{"x":271,"y":292,"fill":181,"style":200},"200",[109,294,279],{"x":271,"y":295,"fill":199,"style":200},"216",[109,297,283],{"x":271,"y":298,"fill":181,"style":282},"232",[176,300],{"x":186,"y":301,"width":266,"height":267,"fill":222,"rx":268,"opacity":252},"270",[109,303,305],{"x":271,"y":304,"fill":181,"style":194},"292","App Instance 3",[109,307,275],{"x":271,"y":308,"fill":181,"style":200},"310",[109,310,279],{"x":271,"y":311,"fill":199,"style":200},"326",[109,313,283],{"x":271,"y":314,"fill":181,"style":282},"342",[316,317],"line",{"x1":318,"y1":319,"x2":320,"y2":321,"stroke":199,"style":322},"220","95","528","155","stroke-width:2;stroke-dasharray:5,3",[316,324],{"x1":318,"y1":325,"x2":320,"y2":326,"stroke":199,"style":322},"205","227",[316,328],{"x1":318,"y1":329,"x2":320,"y2":330,"stroke":199,"style":322},"315","299",[109,332,335],{"x":333,"y":334,"fill":208,"style":200},"355","372","\n3 x 50 = 150, within 155 usable (170 - 15 reserved)\n",[47,337,339],{"id":338},"query-construction-async-execution-patterns","Query Construction & Async Execution Patterns",[52,341,343],{"id":342},"deriving-pool_size-from-instance-class-and-replica-count","Deriving pool_size from instance class and replica count",[14,345,346,347,349],{},"The worked calculation below uses a ",[18,348,77],{}," primary with one read replica and three application instances (e.g., three ECS tasks or three Kubernetes pods).",[104,351,355],{"className":352,"code":353,"language":354,"meta":112,"style":112},"language-python shiki shiki-themes github-light github-dark","from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n\n# db.r6g.large: max_connections ≈ 683\n# Reserve: 3 superuser + 10 admin\u002Fmonitoring = 13\n# Usable: 683 - 13 = 670\n# Two engines per instance: one to primary, one to replica\n# Primary budget: 670 \u002F 2 = 335 across 3 instances → 111 per instance\n# pool_size = floor(111 * 0.7) = 77  max_overflow = 33\n\nPRIMARY_URL = \"postgresql+asyncpg:\u002F\u002Fapp:secret@rds-primary.cluster-xyz.us-east-1.rds.amazonaws.com\u002Forders\"\nREPLICA_URL = \"postgresql+asyncpg:\u002F\u002Fapp:secret@rds-replica.cluster-xyz.us-east-1.rds.amazonaws.com\u002Forders\"\n\nprimary_engine = create_async_engine(\n    PRIMARY_URL,\n    pool_size=77,\n    max_overflow=33,\n    pool_timeout=30.0,\n    pool_recycle=1800,        # 30 min — well below RDS idle timeout of 8 h\n    pool_pre_ping=True,\n    echo=False,\n)\n\nreplica_engine = create_async_engine(\n    REPLICA_URL,\n    pool_size=77,\n    max_overflow=33,\n    pool_timeout=30.0,\n    pool_recycle=1800,\n    pool_pre_ping=True,\n    echo=False,\n)\n\nWriteSession = async_sessionmaker(primary_engine, class_=AsyncSession, expire_on_commit=False)\nReadSession  = async_sessionmaker(replica_engine,  class_=AsyncSession, expire_on_commit=False)\n","python",[18,356,357,375,382,389,395,401,407,413,419,424,438,449,454,466,475,489,502,515,532,545,558,564,569,579,587,598,609,620,631,642,653,658,663,691],{"__ignoreMap":112},[358,359,361,365,369,372],"span",{"class":316,"line":360},1,[358,362,364],{"class":363},"szBVR","from",[358,366,368],{"class":367},"sVt8B"," sqlalchemy.ext.asyncio ",[358,370,371],{"class":363},"import",[358,373,374],{"class":367}," create_async_engine, async_sessionmaker, AsyncSession\n",[358,376,378],{"class":316,"line":377},2,[358,379,381],{"emptyLinePlaceholder":380},true,"\n",[358,383,385],{"class":316,"line":384},3,[358,386,388],{"class":387},"sJ8bj","# db.r6g.large: max_connections ≈ 683\n",[358,390,392],{"class":316,"line":391},4,[358,393,394],{"class":387},"# Reserve: 3 superuser + 10 admin\u002Fmonitoring = 13\n",[358,396,398],{"class":316,"line":397},5,[358,399,400],{"class":387},"# Usable: 683 - 13 = 670\n",[358,402,404],{"class":316,"line":403},6,[358,405,406],{"class":387},"# Two engines per instance: one to primary, one to replica\n",[358,408,410],{"class":316,"line":409},7,[358,411,412],{"class":387},"# Primary budget: 670 \u002F 2 = 335 across 3 instances → 111 per instance\n",[358,414,416],{"class":316,"line":415},8,[358,417,418],{"class":387},"# pool_size = floor(111 * 0.7) = 77  max_overflow = 33\n",[358,420,422],{"class":316,"line":421},9,[358,423,381],{"emptyLinePlaceholder":380},[358,425,427,431,434],{"class":316,"line":426},10,[358,428,430],{"class":429},"sj4cs","PRIMARY_URL",[358,432,433],{"class":363}," =",[358,435,437],{"class":436},"sZZnC"," \"postgresql+asyncpg:\u002F\u002Fapp:secret@rds-primary.cluster-xyz.us-east-1.rds.amazonaws.com\u002Forders\"\n",[358,439,441,444,446],{"class":316,"line":440},11,[358,442,443],{"class":429},"REPLICA_URL",[358,445,433],{"class":363},[358,447,448],{"class":436}," \"postgresql+asyncpg:\u002F\u002Fapp:secret@rds-replica.cluster-xyz.us-east-1.rds.amazonaws.com\u002Forders\"\n",[358,450,452],{"class":316,"line":451},12,[358,453,381],{"emptyLinePlaceholder":380},[358,455,457,460,463],{"class":316,"line":456},13,[358,458,459],{"class":367},"primary_engine ",[358,461,462],{"class":363},"=",[358,464,465],{"class":367}," create_async_engine(\n",[358,467,469,472],{"class":316,"line":468},14,[358,470,471],{"class":429},"    PRIMARY_URL",[358,473,474],{"class":367},",\n",[358,476,478,482,484,487],{"class":316,"line":477},15,[358,479,481],{"class":480},"s4XuR","    pool_size",[358,483,462],{"class":363},[358,485,486],{"class":429},"77",[358,488,474],{"class":367},[358,490,492,495,497,500],{"class":316,"line":491},16,[358,493,494],{"class":480},"    max_overflow",[358,496,462],{"class":363},[358,498,499],{"class":429},"33",[358,501,474],{"class":367},[358,503,505,508,510,513],{"class":316,"line":504},17,[358,506,507],{"class":480},"    pool_timeout",[358,509,462],{"class":363},[358,511,512],{"class":429},"30.0",[358,514,474],{"class":367},[358,516,518,521,523,526,529],{"class":316,"line":517},18,[358,519,520],{"class":480},"    pool_recycle",[358,522,462],{"class":363},[358,524,525],{"class":429},"1800",[358,527,528],{"class":367},",        ",[358,530,531],{"class":387},"# 30 min — well below RDS idle timeout of 8 h\n",[358,533,535,538,540,543],{"class":316,"line":534},19,[358,536,537],{"class":480},"    pool_pre_ping",[358,539,462],{"class":363},[358,541,542],{"class":429},"True",[358,544,474],{"class":367},[358,546,548,551,553,556],{"class":316,"line":547},20,[358,549,550],{"class":480},"    echo",[358,552,462],{"class":363},[358,554,555],{"class":429},"False",[358,557,474],{"class":367},[358,559,561],{"class":316,"line":560},21,[358,562,563],{"class":367},")\n",[358,565,567],{"class":316,"line":566},22,[358,568,381],{"emptyLinePlaceholder":380},[358,570,572,575,577],{"class":316,"line":571},23,[358,573,574],{"class":367},"replica_engine ",[358,576,462],{"class":363},[358,578,465],{"class":367},[358,580,582,585],{"class":316,"line":581},24,[358,583,584],{"class":429},"    REPLICA_URL",[358,586,474],{"class":367},[358,588,590,592,594,596],{"class":316,"line":589},25,[358,591,481],{"class":480},[358,593,462],{"class":363},[358,595,486],{"class":429},[358,597,474],{"class":367},[358,599,601,603,605,607],{"class":316,"line":600},26,[358,602,494],{"class":480},[358,604,462],{"class":363},[358,606,499],{"class":429},[358,608,474],{"class":367},[358,610,612,614,616,618],{"class":316,"line":611},27,[358,613,507],{"class":480},[358,615,462],{"class":363},[358,617,512],{"class":429},[358,619,474],{"class":367},[358,621,623,625,627,629],{"class":316,"line":622},28,[358,624,520],{"class":480},[358,626,462],{"class":363},[358,628,525],{"class":429},[358,630,474],{"class":367},[358,632,634,636,638,640],{"class":316,"line":633},29,[358,635,537],{"class":480},[358,637,462],{"class":363},[358,639,542],{"class":429},[358,641,474],{"class":367},[358,643,645,647,649,651],{"class":316,"line":644},30,[358,646,550],{"class":480},[358,648,462],{"class":363},[358,650,555],{"class":429},[358,652,474],{"class":367},[358,654,656],{"class":316,"line":655},31,[358,657,563],{"class":367},[358,659,661],{"class":316,"line":660},32,[358,662,381],{"emptyLinePlaceholder":380},[358,664,666,669,671,674,677,679,682,685,687,689],{"class":316,"line":665},33,[358,667,668],{"class":367},"WriteSession ",[358,670,462],{"class":363},[358,672,673],{"class":367}," async_sessionmaker(primary_engine, ",[358,675,676],{"class":480},"class_",[358,678,462],{"class":363},[358,680,681],{"class":367},"AsyncSession, ",[358,683,684],{"class":480},"expire_on_commit",[358,686,462],{"class":363},[358,688,555],{"class":429},[358,690,563],{"class":367},[358,692,694,697,699,702,704,706,708,710,712,714],{"class":316,"line":693},34,[358,695,696],{"class":367},"ReadSession  ",[358,698,462],{"class":363},[358,700,701],{"class":367}," async_sessionmaker(replica_engine,  ",[358,703,676],{"class":480},[358,705,462],{"class":363},[358,707,681],{"class":367},[358,709,684],{"class":480},[358,711,462],{"class":363},[358,713,555],{"class":429},[358,715,563],{"class":367},[14,717,718,719,721,722,724],{},"When you add a replica, you divide the connection budget across more engines, not accumulate it. Each replica has its own ",[18,720,61],{}," budget derived from its own instance class, so you can afford to give each replica engine the same ",[18,723,24],{}," as the primary engine.",[52,726,728,730],{"id":727},"pool_recycle-below-the-provider-idle-timeout",[18,729,31],{}," below the provider idle timeout",[14,732,733],{},"Every cloud provider silently terminates TCP connections that sit idle past a threshold:",[735,736,737,755],"table",{},[738,739,740],"thead",{},[741,742,743,747,750],"tr",{},[744,745,746],"th",{},"Provider",[744,748,749],{},"Idle timeout",[744,751,752,753],{},"Recommended ",[18,754,31],{},[756,757,758,772,786,800,812],"tbody",{},[741,759,760,764,767],{},[761,762,763],"td",{},"AWS RDS \u002F Aurora",[761,765,766],{},"8 hours (default)",[761,768,769,771],{},[18,770,525],{}," (30 min)",[741,773,774,777,780],{},[761,775,776],{},"AWS RDS Proxy",[761,778,779],{},"30 min (configurable)",[761,781,782,785],{},[18,783,784],{},"900"," (15 min)",[741,787,788,791,794],{},[761,789,790],{},"GCP Cloud SQL",[761,792,793],{},"10 min (public IP) \u002F 1 h (private IP)",[761,795,796,799],{},[18,797,798],{},"300"," (public IP)",[741,801,802,805,808],{},[761,803,804],{},"Azure Flexible Server",[761,806,807],{},"600 s (10 min)",[761,809,810],{},[18,811,798],{},[741,813,814,817,820],{},[761,815,816],{},"PgBouncer (session mode)",[761,818,819],{},"configurable",[761,821,822],{},[18,823,824],{},"server_idle_timeout - 60 s",[14,826,827,828,830],{},"Setting ",[18,829,31],{}," to half the provider timeout is conservative and safe. A connection recycled after 1800 seconds on RDS has 6.5 hours of remaining idle life — plenty of headroom. Aggressive recycling (below 60 seconds) forces unnecessary TCP handshakes and adds latency; avoid it.",[52,832,834,836],{"id":833},"pool_pre_ping-in-cloud-environments",[18,835,34],{}," in cloud environments",[104,838,840],{"className":352,"code":839,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import create_async_engine\n\n# Cloud SQL (private IP): idle timeout ~1 hour\ncloud_sql_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp:secret@\u002Forders?host=\u002Fcloudsql\u002Fproject:us-central1:instance\",\n    pool_size=20,\n    max_overflow=10,\n    pool_recycle=1800,\n    pool_pre_ping=True,   # issues SELECT 1 before checkout — ~2 ms RTT on private IP\n    echo=False,\n)\n",[18,841,842,853,857,862,871,878,889,900,910,924,934],{"__ignoreMap":112},[358,843,844,846,848,850],{"class":316,"line":360},[358,845,364],{"class":363},[358,847,368],{"class":367},[358,849,371],{"class":363},[358,851,852],{"class":367}," create_async_engine\n",[358,854,855],{"class":316,"line":377},[358,856,381],{"emptyLinePlaceholder":380},[358,858,859],{"class":316,"line":384},[358,860,861],{"class":387},"# Cloud SQL (private IP): idle timeout ~1 hour\n",[358,863,864,867,869],{"class":316,"line":391},[358,865,866],{"class":367},"cloud_sql_engine ",[358,868,462],{"class":363},[358,870,465],{"class":367},[358,872,873,876],{"class":316,"line":397},[358,874,875],{"class":436},"    \"postgresql+asyncpg:\u002F\u002Fapp:secret@\u002Forders?host=\u002Fcloudsql\u002Fproject:us-central1:instance\"",[358,877,474],{"class":367},[358,879,880,882,884,887],{"class":316,"line":403},[358,881,481],{"class":480},[358,883,462],{"class":363},[358,885,886],{"class":429},"20",[358,888,474],{"class":367},[358,890,891,893,895,898],{"class":316,"line":409},[358,892,494],{"class":480},[358,894,462],{"class":363},[358,896,897],{"class":429},"10",[358,899,474],{"class":367},[358,901,902,904,906,908],{"class":316,"line":415},[358,903,520],{"class":480},[358,905,462],{"class":363},[358,907,525],{"class":429},[358,909,474],{"class":367},[358,911,912,914,916,918,921],{"class":316,"line":421},[358,913,537],{"class":480},[358,915,462],{"class":363},[358,917,542],{"class":429},[358,919,920],{"class":367},",   ",[358,922,923],{"class":387},"# issues SELECT 1 before checkout — ~2 ms RTT on private IP\n",[358,925,926,928,930,932],{"class":316,"line":426},[358,927,550],{"class":480},[358,929,462],{"class":363},[358,931,555],{"class":429},[358,933,474],{"class":367},[358,935,936],{"class":316,"line":440},[358,937,563],{"class":367},[14,939,940,943,944,947,948,950,951,953,954,956,957,45],{},[18,941,942],{},"pool_pre_ping=True"," issues a lightweight ",[18,945,946],{},"SELECT 1"," before every connection checkout. If the connection is stale (the provider reset it), SQLAlchemy discards it and fetches a fresh connection transparently. The cost is roughly 1–3 ms per checkout on private-IP cloud networking. For latency-critical paths, use ",[18,949,31],{}," as the primary defence and keep ",[18,952,34],{}," as a safety net rather than the sole mitigation. Detailed usage of ",[18,955,34],{}," for stale connection scenarios is covered in ",[41,958,960],{"href":959},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002Fconfiguring-pool-pre-ping-to-handle-stale-connections\u002F","Configuring Pool Pre-Ping to Handle Stale Connections",[47,962,964],{"id":963},"state-management-session-boundaries","State Management & Session Boundaries",[14,966,967,968,971,972,975],{},"Async SQLAlchemy sessions are not thread-safe and must never be shared across request boundaries or async tasks. Each request acquires a session from the factory, performs its work within a ",[18,969,970],{},"begin()"," block, and releases the session — which returns its checked-out connection to the pool. The connection does not return to the pool until the ",[18,973,974],{},"AsyncSession"," context manager exits.",[104,977,979],{"className":352,"code":978,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import AsyncSession\nfrom sqlalchemy import select\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nimport uuid\n\n\nclass Base(DeclarativeBase):\n    pass\n\n\nclass Order(Base):\n    __tablename__ = \"orders\"\n    id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)\n    tenant_id: Mapped[str]\n    total_cents: Mapped[int]\n\n\nasync def fetch_tenant_orders(session: AsyncSession, tenant_id: str) -> list[Order]:\n    \"\"\"Connection is checked out when this coroutine begins executing the query\n    and returned when the caller's session context manager exits.\"\"\"\n    stmt = select(Order).where(Order.tenant_id == tenant_id)\n    result = await session.execute(stmt)\n    return list(result.scalars())\n",[18,980,981,992,1004,1016,1023,1027,1031,1049,1054,1058,1062,1076,1086,1122,1133,1143,1147,1151,1170,1175,1180,1196,1209],{"__ignoreMap":112},[358,982,983,985,987,989],{"class":316,"line":360},[358,984,364],{"class":363},[358,986,368],{"class":367},[358,988,371],{"class":363},[358,990,991],{"class":367}," AsyncSession\n",[358,993,994,996,999,1001],{"class":316,"line":377},[358,995,364],{"class":363},[358,997,998],{"class":367}," sqlalchemy ",[358,1000,371],{"class":363},[358,1002,1003],{"class":367}," select\n",[358,1005,1006,1008,1011,1013],{"class":316,"line":384},[358,1007,364],{"class":363},[358,1009,1010],{"class":367}," sqlalchemy.orm ",[358,1012,371],{"class":363},[358,1014,1015],{"class":367}," DeclarativeBase, Mapped, mapped_column\n",[358,1017,1018,1020],{"class":316,"line":391},[358,1019,371],{"class":363},[358,1021,1022],{"class":367}," uuid\n",[358,1024,1025],{"class":316,"line":397},[358,1026,381],{"emptyLinePlaceholder":380},[358,1028,1029],{"class":316,"line":403},[358,1030,381],{"emptyLinePlaceholder":380},[358,1032,1033,1036,1040,1043,1046],{"class":316,"line":409},[358,1034,1035],{"class":363},"class",[358,1037,1039],{"class":1038},"sScJk"," Base",[358,1041,1042],{"class":367},"(",[358,1044,1045],{"class":1038},"DeclarativeBase",[358,1047,1048],{"class":367},"):\n",[358,1050,1051],{"class":316,"line":415},[358,1052,1053],{"class":363},"    pass\n",[358,1055,1056],{"class":316,"line":421},[358,1057,381],{"emptyLinePlaceholder":380},[358,1059,1060],{"class":316,"line":426},[358,1061,381],{"emptyLinePlaceholder":380},[358,1063,1064,1066,1069,1071,1074],{"class":316,"line":440},[358,1065,1035],{"class":363},[358,1067,1068],{"class":1038}," Order",[358,1070,1042],{"class":367},[358,1072,1073],{"class":1038},"Base",[358,1075,1048],{"class":367},[358,1077,1078,1081,1083],{"class":316,"line":451},[358,1079,1080],{"class":367},"    __tablename__ ",[358,1082,462],{"class":363},[358,1084,1085],{"class":436}," \"orders\"\n",[358,1087,1088,1091,1094,1097,1100,1102,1105,1108,1110,1112,1114,1117,1119],{"class":316,"line":456},[358,1089,1090],{"class":429},"    id",[358,1092,1093],{"class":367},": Mapped[uuid.",[358,1095,1096],{"class":429},"UUID",[358,1098,1099],{"class":367},"] ",[358,1101,462],{"class":363},[358,1103,1104],{"class":367}," mapped_column(",[358,1106,1107],{"class":480},"primary_key",[358,1109,462],{"class":363},[358,1111,542],{"class":429},[358,1113,25],{"class":367},[358,1115,1116],{"class":480},"default",[358,1118,462],{"class":363},[358,1120,1121],{"class":367},"uuid.uuid4)\n",[358,1123,1124,1127,1130],{"class":316,"line":468},[358,1125,1126],{"class":367},"    tenant_id: Mapped[",[358,1128,1129],{"class":429},"str",[358,1131,1132],{"class":367},"]\n",[358,1134,1135,1138,1141],{"class":316,"line":477},[358,1136,1137],{"class":367},"    total_cents: Mapped[",[358,1139,1140],{"class":429},"int",[358,1142,1132],{"class":367},[358,1144,1145],{"class":316,"line":491},[358,1146,381],{"emptyLinePlaceholder":380},[358,1148,1149],{"class":316,"line":504},[358,1150,381],{"emptyLinePlaceholder":380},[358,1152,1153,1156,1159,1162,1165,1167],{"class":316,"line":517},[358,1154,1155],{"class":363},"async",[358,1157,1158],{"class":363}," def",[358,1160,1161],{"class":1038}," fetch_tenant_orders",[358,1163,1164],{"class":367},"(session: AsyncSession, tenant_id: ",[358,1166,1129],{"class":429},[358,1168,1169],{"class":367},") -> list[Order]:\n",[358,1171,1172],{"class":316,"line":534},[358,1173,1174],{"class":436},"    \"\"\"Connection is checked out when this coroutine begins executing the query\n",[358,1176,1177],{"class":316,"line":547},[358,1178,1179],{"class":436},"    and returned when the caller's session context manager exits.\"\"\"\n",[358,1181,1182,1185,1187,1190,1193],{"class":316,"line":560},[358,1183,1184],{"class":367},"    stmt ",[358,1186,462],{"class":363},[358,1188,1189],{"class":367}," select(Order).where(Order.tenant_id ",[358,1191,1192],{"class":363},"==",[358,1194,1195],{"class":367}," tenant_id)\n",[358,1197,1198,1201,1203,1206],{"class":316,"line":566},[358,1199,1200],{"class":367},"    result ",[358,1202,462],{"class":363},[358,1204,1205],{"class":363}," await",[358,1207,1208],{"class":367}," session.execute(stmt)\n",[358,1210,1211,1214,1217],{"class":316,"line":571},[358,1212,1213],{"class":363},"    return",[358,1215,1216],{"class":429}," list",[358,1218,1219],{"class":367},"(result.scalars())\n",[14,1221,1222],{},"Long-running transactions hold a connection for their entire duration. In cloud environments, this is especially expensive because the provider may enforce statement timeouts at the proxy layer (RDS Proxy: 600 s by default). Structure transactions to be as short as possible: fetch, transform, write, commit — never hold a connection open while waiting for external I\u002FO (HTTP calls, message queue acks).",[52,1224,1226],{"id":1225},"observing-pool-utilisation-in-real-time","Observing pool utilisation in real time",[14,1228,1229,1230,1233,1234,1237,1238,1241,1242,1244,1245,1248,1249,1251],{},"SQLAlchemy exposes pool introspection through the sync engine's pool object. In an async engine, access it via ",[18,1231,1232],{},"engine.sync_engine.pool",". The three key metrics are ",[18,1235,1236],{},"checkedout()"," (connections currently in use), ",[18,1239,1240],{},"overflow()"," (connections beyond ",[18,1243,24],{}," that are in use), and ",[18,1246,1247],{},"size()"," (the configured ",[18,1250,24],{},"). Emit these to Prometheus or CloudWatch at regular intervals during load testing to confirm the pool operates within budget.",[104,1253,1255],{"className":352,"code":1254,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import AsyncEngine\nfrom sqlalchemy import event\n\n\ndef register_pool_metrics(engine: AsyncEngine) -> None:\n    \"\"\"Attach event listeners to emit pool utilisation on every checkout and checkin.\"\"\"\n    import logging\n    logger = logging.getLogger(\"app.pool\")\n\n    @event.listens_for(engine.sync_engine, \"checkout\")\n    def on_checkout(dbapi_conn, conn_record, conn_proxy) -> None:\n        pool = engine.sync_engine.pool\n        logger.debug(\n            \"pool checkout\",\n            extra={\n                \"checked_out\": pool.checkedout(),\n                \"overflow\": pool.overflow(),\n                \"pool_size\": pool.size(),\n            },\n        )\n\n    @event.listens_for(engine.sync_engine, \"checkin\")\n    def on_checkin(dbapi_conn, conn_record) -> None:\n        pool = engine.sync_engine.pool\n        if pool.checkedout() > pool.size() * 0.8:\n            logger.warning(\n                \"pool utilisation above 80%%: checked_out=%d pool_size=%d\",\n                pool.checkedout(),\n                pool.size(),\n            )\n",[18,1256,1257,1268,1279,1283,1287,1304,1309,1317,1332,1336,1349,1364,1374,1379,1386,1396,1404,1412,1420,1425,1430,1434,1445,1459,1467,1489,1494,1518,1523,1528],{"__ignoreMap":112},[358,1258,1259,1261,1263,1265],{"class":316,"line":360},[358,1260,364],{"class":363},[358,1262,368],{"class":367},[358,1264,371],{"class":363},[358,1266,1267],{"class":367}," AsyncEngine\n",[358,1269,1270,1272,1274,1276],{"class":316,"line":377},[358,1271,364],{"class":363},[358,1273,998],{"class":367},[358,1275,371],{"class":363},[358,1277,1278],{"class":367}," event\n",[358,1280,1281],{"class":316,"line":384},[358,1282,381],{"emptyLinePlaceholder":380},[358,1284,1285],{"class":316,"line":391},[358,1286,381],{"emptyLinePlaceholder":380},[358,1288,1289,1292,1295,1298,1301],{"class":316,"line":397},[358,1290,1291],{"class":363},"def",[358,1293,1294],{"class":1038}," register_pool_metrics",[358,1296,1297],{"class":367},"(engine: AsyncEngine) -> ",[358,1299,1300],{"class":429},"None",[358,1302,1303],{"class":367},":\n",[358,1305,1306],{"class":316,"line":403},[358,1307,1308],{"class":436},"    \"\"\"Attach event listeners to emit pool utilisation on every checkout and checkin.\"\"\"\n",[358,1310,1311,1314],{"class":316,"line":409},[358,1312,1313],{"class":363},"    import",[358,1315,1316],{"class":367}," logging\n",[358,1318,1319,1322,1324,1327,1330],{"class":316,"line":415},[358,1320,1321],{"class":367},"    logger ",[358,1323,462],{"class":363},[358,1325,1326],{"class":367}," logging.getLogger(",[358,1328,1329],{"class":436},"\"app.pool\"",[358,1331,563],{"class":367},[358,1333,1334],{"class":316,"line":421},[358,1335,381],{"emptyLinePlaceholder":380},[358,1337,1338,1341,1344,1347],{"class":316,"line":426},[358,1339,1340],{"class":1038},"    @event.listens_for",[358,1342,1343],{"class":367},"(engine.sync_engine, ",[358,1345,1346],{"class":436},"\"checkout\"",[358,1348,563],{"class":367},[358,1350,1351,1354,1357,1360,1362],{"class":316,"line":440},[358,1352,1353],{"class":363},"    def",[358,1355,1356],{"class":1038}," on_checkout",[358,1358,1359],{"class":367},"(dbapi_conn, conn_record, conn_proxy) -> ",[358,1361,1300],{"class":429},[358,1363,1303],{"class":367},[358,1365,1366,1369,1371],{"class":316,"line":451},[358,1367,1368],{"class":367},"        pool ",[358,1370,462],{"class":363},[358,1372,1373],{"class":367}," engine.sync_engine.pool\n",[358,1375,1376],{"class":316,"line":456},[358,1377,1378],{"class":367},"        logger.debug(\n",[358,1380,1381,1384],{"class":316,"line":468},[358,1382,1383],{"class":436},"            \"pool checkout\"",[358,1385,474],{"class":367},[358,1387,1388,1391,1393],{"class":316,"line":477},[358,1389,1390],{"class":480},"            extra",[358,1392,462],{"class":363},[358,1394,1395],{"class":367},"{\n",[358,1397,1398,1401],{"class":316,"line":491},[358,1399,1400],{"class":436},"                \"checked_out\"",[358,1402,1403],{"class":367},": pool.checkedout(),\n",[358,1405,1406,1409],{"class":316,"line":504},[358,1407,1408],{"class":436},"                \"overflow\"",[358,1410,1411],{"class":367},": pool.overflow(),\n",[358,1413,1414,1417],{"class":316,"line":517},[358,1415,1416],{"class":436},"                \"pool_size\"",[358,1418,1419],{"class":367},": pool.size(),\n",[358,1421,1422],{"class":316,"line":534},[358,1423,1424],{"class":367},"            },\n",[358,1426,1427],{"class":316,"line":547},[358,1428,1429],{"class":367},"        )\n",[358,1431,1432],{"class":316,"line":560},[358,1433,381],{"emptyLinePlaceholder":380},[358,1435,1436,1438,1440,1443],{"class":316,"line":566},[358,1437,1340],{"class":1038},[358,1439,1343],{"class":367},[358,1441,1442],{"class":436},"\"checkin\"",[358,1444,563],{"class":367},[358,1446,1447,1449,1452,1455,1457],{"class":316,"line":571},[358,1448,1353],{"class":363},[358,1450,1451],{"class":1038}," on_checkin",[358,1453,1454],{"class":367},"(dbapi_conn, conn_record) -> ",[358,1456,1300],{"class":429},[358,1458,1303],{"class":367},[358,1460,1461,1463,1465],{"class":316,"line":581},[358,1462,1368],{"class":367},[358,1464,462],{"class":363},[358,1466,1373],{"class":367},[358,1468,1469,1472,1475,1478,1481,1484,1487],{"class":316,"line":589},[358,1470,1471],{"class":363},"        if",[358,1473,1474],{"class":367}," pool.checkedout() ",[358,1476,1477],{"class":363},">",[358,1479,1480],{"class":367}," pool.size() ",[358,1482,1483],{"class":363},"*",[358,1485,1486],{"class":429}," 0.8",[358,1488,1303],{"class":367},[358,1490,1491],{"class":316,"line":600},[358,1492,1493],{"class":367},"            logger.warning(\n",[358,1495,1496,1499,1502,1505,1508,1511,1513,1516],{"class":316,"line":611},[358,1497,1498],{"class":436},"                \"pool utilisation above 80",[358,1500,1501],{"class":429},"%%",[358,1503,1504],{"class":436},": checked_out=",[358,1506,1507],{"class":429},"%d",[358,1509,1510],{"class":436}," pool_size=",[358,1512,1507],{"class":429},[358,1514,1515],{"class":436},"\"",[358,1517,474],{"class":367},[358,1519,1520],{"class":316,"line":622},[358,1521,1522],{"class":367},"                pool.checkedout(),\n",[358,1524,1525],{"class":316,"line":633},[358,1526,1527],{"class":367},"                pool.size(),\n",[358,1529,1530],{"class":316,"line":644},[358,1531,1532],{"class":367},"            )\n",[14,1534,1535,1536,1538,1539,1541,1542,1544,1545,1548],{},"A sustained ",[18,1537,1236],{}," value above 80 % of ",[18,1540,24],{}," is a strong signal that either ",[18,1543,24],{}," is too small for the workload, or that long-running transactions are holding connections longer than expected. Investigate ",[18,1546,1547],{},"pg_stat_activity"," to determine which queries are occupying connections and for how long before adjusting pool parameters.",[47,1550,1552],{"id":1551},"advanced-cloud-specific-patterns","Advanced Cloud-Specific Patterns",[52,1554,1556],{"id":1555},"behavior-behind-pgbouncer-in-transaction-pooling-mode","Behavior behind PgBouncer in transaction pooling mode",[14,1558,1559,1560,1563],{},"PgBouncer in ",[87,1561,1562],{},"transaction pooling mode"," is the most common configuration for cloud deployments because it multiplexes many client connections over far fewer server connections. This is a fundamental shift: your SQLAlchemy pool is now talking to PgBouncer, not directly to Postgres. PgBouncer assigns a real server connection for the duration of each transaction, then releases it back to its own pool.",[14,1565,1566],{},"This creates two critical constraints:",[1568,1569,1570,1581],"ol",{},[1571,1572,1573,1576,1577,1580],"li",{},[87,1574,1575],{},"Prepared statements cannot be used."," PgBouncer does not route prepared statements back to the same server connection in transaction mode, causing ",[18,1578,1579],{},"asyncpg.exceptions.InvalidSQLStatementNameError",". You must disable asyncpg's prepared statement cache entirely.",[1571,1582,1583,1586,1587,1590,1591,1593],{},[87,1584,1585],{},"Client-side pooling is redundant and wasteful."," If PgBouncer is pooling at the server level, maintaining a large SQLAlchemy ",[18,1588,1589],{},"QueuePool"," on the client side just increases the number of client connections PgBouncer has to track. Use ",[18,1592,38],{}," or a pool of 1–2 connections per process.",[104,1595,1597],{"className":352,"code":1596,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import create_async_engine\nfrom sqlalchemy.pool import NullPool\n\n# Behind PgBouncer in transaction pooling mode\npgbouncer_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp:secret@pgbouncer.internal:6432\u002Forders\",\n    poolclass=NullPool,          # let PgBouncer manage all pooling\n    connect_args={\n        \"statement_cache_size\": 0,          # disable asyncpg prepared statement cache\n        \"prepared_statement_cache_size\": 0, # belt-and-suspenders for older asyncpg\n        \"server_settings\": {\n            \"jit\": \"off\",        # JIT and prepared statements conflict in pgbouncer mode\n        },\n    },\n    echo=False,\n)\n",[18,1598,1599,1609,1621,1625,1630,1639,1646,1659,1668,1684,1698,1706,1721,1726,1731,1741],{"__ignoreMap":112},[358,1600,1601,1603,1605,1607],{"class":316,"line":360},[358,1602,364],{"class":363},[358,1604,368],{"class":367},[358,1606,371],{"class":363},[358,1608,852],{"class":367},[358,1610,1611,1613,1616,1618],{"class":316,"line":377},[358,1612,364],{"class":363},[358,1614,1615],{"class":367}," sqlalchemy.pool ",[358,1617,371],{"class":363},[358,1619,1620],{"class":367}," NullPool\n",[358,1622,1623],{"class":316,"line":384},[358,1624,381],{"emptyLinePlaceholder":380},[358,1626,1627],{"class":316,"line":391},[358,1628,1629],{"class":387},"# Behind PgBouncer in transaction pooling mode\n",[358,1631,1632,1635,1637],{"class":316,"line":397},[358,1633,1634],{"class":367},"pgbouncer_engine ",[358,1636,462],{"class":363},[358,1638,465],{"class":367},[358,1640,1641,1644],{"class":316,"line":403},[358,1642,1643],{"class":436},"    \"postgresql+asyncpg:\u002F\u002Fapp:secret@pgbouncer.internal:6432\u002Forders\"",[358,1645,474],{"class":367},[358,1647,1648,1651,1653,1656],{"class":316,"line":409},[358,1649,1650],{"class":480},"    poolclass",[358,1652,462],{"class":363},[358,1654,1655],{"class":367},"NullPool,          ",[358,1657,1658],{"class":387},"# let PgBouncer manage all pooling\n",[358,1660,1661,1664,1666],{"class":316,"line":415},[358,1662,1663],{"class":480},"    connect_args",[358,1665,462],{"class":363},[358,1667,1395],{"class":367},[358,1669,1670,1673,1676,1678,1681],{"class":316,"line":421},[358,1671,1672],{"class":436},"        \"statement_cache_size\"",[358,1674,1675],{"class":367},": ",[358,1677,178],{"class":429},[358,1679,1680],{"class":367},",          ",[358,1682,1683],{"class":387},"# disable asyncpg prepared statement cache\n",[358,1685,1686,1689,1691,1693,1695],{"class":316,"line":426},[358,1687,1688],{"class":436},"        \"prepared_statement_cache_size\"",[358,1690,1675],{"class":367},[358,1692,178],{"class":429},[358,1694,25],{"class":367},[358,1696,1697],{"class":387},"# belt-and-suspenders for older asyncpg\n",[358,1699,1700,1703],{"class":316,"line":440},[358,1701,1702],{"class":436},"        \"server_settings\"",[358,1704,1705],{"class":367},": {\n",[358,1707,1708,1711,1713,1716,1718],{"class":316,"line":451},[358,1709,1710],{"class":436},"            \"jit\"",[358,1712,1675],{"class":367},[358,1714,1715],{"class":436},"\"off\"",[358,1717,528],{"class":367},[358,1719,1720],{"class":387},"# JIT and prepared statements conflict in pgbouncer mode\n",[358,1722,1723],{"class":316,"line":456},[358,1724,1725],{"class":367},"        },\n",[358,1727,1728],{"class":316,"line":468},[358,1729,1730],{"class":367},"    },\n",[358,1732,1733,1735,1737,1739],{"class":316,"line":477},[358,1734,550],{"class":480},[358,1736,462],{"class":363},[358,1738,555],{"class":429},[358,1740,474],{"class":367},[358,1742,1743],{"class":316,"line":491},[358,1744,563],{"class":367},[14,1746,1747,1748,1750,1751,1754,1755,1758,1759,45],{},"The same ",[18,1749,38],{}," + ",[18,1752,1753],{},"statement_cache_size=0"," combination applies when using the ",[87,1756,1757],{},"Cloud SQL Auth Proxy"," in transaction mode, or when PgBouncer sits between your app and Aurora serverless v2. For a deeper treatment of asyncpg prepared-statement errors with PgBouncer, see ",[41,1760,1762],{"href":1761},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002Fhandling-asyncpg-prepared-statement-errors-with-pgbouncer\u002F","Handling asyncpg Prepared Statement Errors with PgBouncer",[52,1764,1766],{"id":1765},"rds-proxy","RDS Proxy",[14,1768,1769],{},"AWS RDS Proxy acts as a managed PgBouncer for RDS and Aurora. It multiplexes connections using transaction-level pinning by default. Unlike raw PgBouncer, RDS Proxy supports prepared statements through a pinning mechanism — but pinning a client connection to a server connection defeats most of the proxy's multiplexing benefit. The practical guidance is identical: disable prepared statement caching or use session-mode pinning deliberately.",[104,1771,1773],{"className":352,"code":1772,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import create_async_engine\n\n# RDS Proxy endpoint — IAM auth via token in password\nrds_proxy_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp:{iam_token}@myapp.proxy-abc.us-east-1.rds.amazonaws.com\u002Forders\",\n    pool_size=5,               # small client-side pool; proxy handles the rest\n    max_overflow=2,\n    pool_timeout=15.0,\n    pool_recycle=900,          # proxy's default idle client timeout is 1800 s; recycle below it\n    pool_pre_ping=True,\n    connect_args={\n        \"statement_cache_size\": 0,\n        \"ssl\": True,           # RDS Proxy requires TLS\n    },\n    echo=False,\n)\n",[18,1774,1775,1785,1789,1794,1803,1816,1831,1842,1853,1866,1876,1884,1894,1909,1913,1923],{"__ignoreMap":112},[358,1776,1777,1779,1781,1783],{"class":316,"line":360},[358,1778,364],{"class":363},[358,1780,368],{"class":367},[358,1782,371],{"class":363},[358,1784,852],{"class":367},[358,1786,1787],{"class":316,"line":377},[358,1788,381],{"emptyLinePlaceholder":380},[358,1790,1791],{"class":316,"line":384},[358,1792,1793],{"class":387},"# RDS Proxy endpoint — IAM auth via token in password\n",[358,1795,1796,1799,1801],{"class":316,"line":391},[358,1797,1798],{"class":367},"rds_proxy_engine ",[358,1800,462],{"class":363},[358,1802,465],{"class":367},[358,1804,1805,1808,1811,1814],{"class":316,"line":397},[358,1806,1807],{"class":436},"    \"postgresql+asyncpg:\u002F\u002Fapp:",[358,1809,1810],{"class":429},"{iam_token}",[358,1812,1813],{"class":436},"@myapp.proxy-abc.us-east-1.rds.amazonaws.com\u002Forders\"",[358,1815,474],{"class":367},[358,1817,1818,1820,1822,1825,1828],{"class":316,"line":403},[358,1819,481],{"class":480},[358,1821,462],{"class":363},[358,1823,1824],{"class":429},"5",[358,1826,1827],{"class":367},",               ",[358,1829,1830],{"class":387},"# small client-side pool; proxy handles the rest\n",[358,1832,1833,1835,1837,1840],{"class":316,"line":409},[358,1834,494],{"class":480},[358,1836,462],{"class":363},[358,1838,1839],{"class":429},"2",[358,1841,474],{"class":367},[358,1843,1844,1846,1848,1851],{"class":316,"line":415},[358,1845,507],{"class":480},[358,1847,462],{"class":363},[358,1849,1850],{"class":429},"15.0",[358,1852,474],{"class":367},[358,1854,1855,1857,1859,1861,1863],{"class":316,"line":421},[358,1856,520],{"class":480},[358,1858,462],{"class":363},[358,1860,784],{"class":429},[358,1862,1680],{"class":367},[358,1864,1865],{"class":387},"# proxy's default idle client timeout is 1800 s; recycle below it\n",[358,1867,1868,1870,1872,1874],{"class":316,"line":426},[358,1869,537],{"class":480},[358,1871,462],{"class":363},[358,1873,542],{"class":429},[358,1875,474],{"class":367},[358,1877,1878,1880,1882],{"class":316,"line":440},[358,1879,1663],{"class":480},[358,1881,462],{"class":363},[358,1883,1395],{"class":367},[358,1885,1886,1888,1890,1892],{"class":316,"line":451},[358,1887,1672],{"class":436},[358,1889,1675],{"class":367},[358,1891,178],{"class":429},[358,1893,474],{"class":367},[358,1895,1896,1899,1901,1903,1906],{"class":316,"line":456},[358,1897,1898],{"class":436},"        \"ssl\"",[358,1900,1675],{"class":367},[358,1902,542],{"class":429},[358,1904,1905],{"class":367},",           ",[358,1907,1908],{"class":387},"# RDS Proxy requires TLS\n",[358,1910,1911],{"class":316,"line":468},[358,1912,1730],{"class":367},[358,1914,1915,1917,1919,1921],{"class":316,"line":477},[358,1916,550],{"class":480},[358,1918,462],{"class":363},[358,1920,555],{"class":429},[358,1922,474],{"class":367},[358,1924,1925],{"class":316,"line":491},[358,1926,563],{"class":367},[14,1928,1929,1930,1932],{},"Keep ",[18,1931,24],{}," small (3–10) when targeting RDS Proxy. The proxy holds real database connections that are amortized across all clients; a large client-side pool just wastes proxy-side slots.",[52,1934,1936],{"id":1935},"serverless-functions-lambda-and-cloud-run","Serverless functions — Lambda and Cloud Run",[14,1938,1939,1940,1942],{},"Serverless functions have no persistent process, so a warm pool is meaningless between invocations. Each invocation that requires a database connection will create a new connection unless the function container is reused. The only viable strategy is ",[18,1941,38],{}," combined with an external connection pooler (RDS Proxy, PgBouncer, Supabase pgbouncer) to protect the database from connection storms.",[104,1944,1946],{"className":352,"code":1945,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker\nfrom sqlalchemy.pool import NullPool\n\n\n# Lambda \u002F Cloud Run: NullPool — no persistent pool between invocations\nlambda_engine = create_async_engine(\n    \"postgresql+asyncpg:\u002F\u002Fapp:secret@myapp.proxy-abc.us-east-1.rds.amazonaws.com\u002Forders\",\n    poolclass=NullPool,\n    connect_args={\n        \"statement_cache_size\": 0,\n        \"ssl\": True,\n        \"command_timeout\": 5,   # fail fast — Lambda timeout is often 30 s\n    },\n    echo=False,\n)\n\nLambdaSession = async_sessionmaker(lambda_engine, class_=AsyncSession, expire_on_commit=False)\n\n\nasync def lambda_handler(event: dict, context: object) -> dict:\n    async with LambdaSession() as session:\n        async with session.begin():\n            result = await session.execute(\n                select(Order).where(Order.tenant_id == event[\"tenant_id\"]).limit(100)\n            )\n            orders = result.scalars().all()\n    return {\"count\": len(orders)}\n",[18,1947,1948,1959,1969,1973,1977,1982,1991,1998,2007,2015,2025,2035,2049,2053,2063,2067,2071,2095,2099,2103,2131,2148,2158,2170,2190,2194,2204],{"__ignoreMap":112},[358,1949,1950,1952,1954,1956],{"class":316,"line":360},[358,1951,364],{"class":363},[358,1953,368],{"class":367},[358,1955,371],{"class":363},[358,1957,1958],{"class":367}," create_async_engine, AsyncSession, async_sessionmaker\n",[358,1960,1961,1963,1965,1967],{"class":316,"line":377},[358,1962,364],{"class":363},[358,1964,1615],{"class":367},[358,1966,371],{"class":363},[358,1968,1620],{"class":367},[358,1970,1971],{"class":316,"line":384},[358,1972,381],{"emptyLinePlaceholder":380},[358,1974,1975],{"class":316,"line":391},[358,1976,381],{"emptyLinePlaceholder":380},[358,1978,1979],{"class":316,"line":397},[358,1980,1981],{"class":387},"# Lambda \u002F Cloud Run: NullPool — no persistent pool between invocations\n",[358,1983,1984,1987,1989],{"class":316,"line":403},[358,1985,1986],{"class":367},"lambda_engine ",[358,1988,462],{"class":363},[358,1990,465],{"class":367},[358,1992,1993,1996],{"class":316,"line":409},[358,1994,1995],{"class":436},"    \"postgresql+asyncpg:\u002F\u002Fapp:secret@myapp.proxy-abc.us-east-1.rds.amazonaws.com\u002Forders\"",[358,1997,474],{"class":367},[358,1999,2000,2002,2004],{"class":316,"line":415},[358,2001,1650],{"class":480},[358,2003,462],{"class":363},[358,2005,2006],{"class":367},"NullPool,\n",[358,2008,2009,2011,2013],{"class":316,"line":421},[358,2010,1663],{"class":480},[358,2012,462],{"class":363},[358,2014,1395],{"class":367},[358,2016,2017,2019,2021,2023],{"class":316,"line":426},[358,2018,1672],{"class":436},[358,2020,1675],{"class":367},[358,2022,178],{"class":429},[358,2024,474],{"class":367},[358,2026,2027,2029,2031,2033],{"class":316,"line":440},[358,2028,1898],{"class":436},[358,2030,1675],{"class":367},[358,2032,542],{"class":429},[358,2034,474],{"class":367},[358,2036,2037,2040,2042,2044,2046],{"class":316,"line":451},[358,2038,2039],{"class":436},"        \"command_timeout\"",[358,2041,1675],{"class":367},[358,2043,1824],{"class":429},[358,2045,920],{"class":367},[358,2047,2048],{"class":387},"# fail fast — Lambda timeout is often 30 s\n",[358,2050,2051],{"class":316,"line":456},[358,2052,1730],{"class":367},[358,2054,2055,2057,2059,2061],{"class":316,"line":468},[358,2056,550],{"class":480},[358,2058,462],{"class":363},[358,2060,555],{"class":429},[358,2062,474],{"class":367},[358,2064,2065],{"class":316,"line":477},[358,2066,563],{"class":367},[358,2068,2069],{"class":316,"line":491},[358,2070,381],{"emptyLinePlaceholder":380},[358,2072,2073,2076,2078,2081,2083,2085,2087,2089,2091,2093],{"class":316,"line":504},[358,2074,2075],{"class":367},"LambdaSession ",[358,2077,462],{"class":363},[358,2079,2080],{"class":367}," async_sessionmaker(lambda_engine, ",[358,2082,676],{"class":480},[358,2084,462],{"class":363},[358,2086,681],{"class":367},[358,2088,684],{"class":480},[358,2090,462],{"class":363},[358,2092,555],{"class":429},[358,2094,563],{"class":367},[358,2096,2097],{"class":316,"line":517},[358,2098,381],{"emptyLinePlaceholder":380},[358,2100,2101],{"class":316,"line":534},[358,2102,381],{"emptyLinePlaceholder":380},[358,2104,2105,2107,2109,2112,2115,2118,2121,2124,2127,2129],{"class":316,"line":547},[358,2106,1155],{"class":363},[358,2108,1158],{"class":363},[358,2110,2111],{"class":1038}," lambda_handler",[358,2113,2114],{"class":367},"(event: ",[358,2116,2117],{"class":429},"dict",[358,2119,2120],{"class":367},", context: ",[358,2122,2123],{"class":429},"object",[358,2125,2126],{"class":367},") -> ",[358,2128,2117],{"class":429},[358,2130,1303],{"class":367},[358,2132,2133,2136,2139,2142,2145],{"class":316,"line":560},[358,2134,2135],{"class":363},"    async",[358,2137,2138],{"class":363}," with",[358,2140,2141],{"class":367}," LambdaSession() ",[358,2143,2144],{"class":363},"as",[358,2146,2147],{"class":367}," session:\n",[358,2149,2150,2153,2155],{"class":316,"line":566},[358,2151,2152],{"class":363},"        async",[358,2154,2138],{"class":363},[358,2156,2157],{"class":367}," session.begin():\n",[358,2159,2160,2163,2165,2167],{"class":316,"line":571},[358,2161,2162],{"class":367},"            result ",[358,2164,462],{"class":363},[358,2166,1205],{"class":363},[358,2168,2169],{"class":367}," session.execute(\n",[358,2171,2172,2175,2177,2180,2183,2186,2188],{"class":316,"line":581},[358,2173,2174],{"class":367},"                select(Order).where(Order.tenant_id ",[358,2176,1192],{"class":363},[358,2178,2179],{"class":367}," event[",[358,2181,2182],{"class":436},"\"tenant_id\"",[358,2184,2185],{"class":367},"]).limit(",[358,2187,212],{"class":429},[358,2189,563],{"class":367},[358,2191,2192],{"class":316,"line":589},[358,2193,1532],{"class":367},[358,2195,2196,2199,2201],{"class":316,"line":600},[358,2197,2198],{"class":367},"            orders ",[358,2200,462],{"class":363},[358,2202,2203],{"class":367}," result.scalars().all()\n",[358,2205,2206,2208,2211,2214,2216,2219],{"class":316,"line":611},[358,2207,1213],{"class":363},[358,2209,2210],{"class":367}," {",[358,2212,2213],{"class":436},"\"count\"",[358,2215,1675],{"class":367},[358,2217,2218],{"class":429},"len",[358,2220,2221],{"class":367},"(orders)}\n",[14,2223,2224,2225,2227,2228,2231,2232,2234],{},"Without ",[18,2226,38],{},", the first warm-container invocation creates a pool of N connections and holds them. If 500 Lambda instances start simultaneously (a concurrency spike), Postgres receives up to ",[18,2229,2230],{},"500 × pool_size"," connection requests in seconds — a connection storm. The external proxy absorbs this with ",[18,2233,38],{}," on the client side.",[52,2236,2238],{"id":2237},"gcp-cloud-sql-auth-proxy","GCP Cloud SQL Auth Proxy",[14,2240,2241,2242,45],{},"The Cloud SQL Auth Proxy manages IAM authentication and TLS termination, routing connections to Cloud SQL through a local Unix socket or loopback TCP port. It operates in session mode by default, meaning each client connection maps 1:1 to a server connection. There is no built-in transaction multiplexing, so maintain a normal ",[18,2243,1589],{},[104,2245,2247],{"className":352,"code":2246,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import create_async_engine\n\n# Cloud SQL Auth Proxy listening on local Unix socket\ncloud_sql_engine = create_async_engine(\n    # Use host= in connect_args for Unix socket path; URL host is ignored\n    \"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Forders\",\n    pool_size=15,\n    max_overflow=5,\n    pool_recycle=300,          # Cloud SQL public IP idle timeout is 10 min\n    pool_pre_ping=True,\n    connect_args={\n        \"host\": \"\u002Fcloudsql\u002Fmy-project:us-central1:my-instance\",\n        \"ssl\": False,          # proxy handles TLS end-to-end\n    },\n    echo=False,\n)\n",[18,2248,2249,2259,2263,2268,2276,2281,2288,2299,2309,2322,2332,2340,2352,2365,2369,2379],{"__ignoreMap":112},[358,2250,2251,2253,2255,2257],{"class":316,"line":360},[358,2252,364],{"class":363},[358,2254,368],{"class":367},[358,2256,371],{"class":363},[358,2258,852],{"class":367},[358,2260,2261],{"class":316,"line":377},[358,2262,381],{"emptyLinePlaceholder":380},[358,2264,2265],{"class":316,"line":384},[358,2266,2267],{"class":387},"# Cloud SQL Auth Proxy listening on local Unix socket\n",[358,2269,2270,2272,2274],{"class":316,"line":391},[358,2271,866],{"class":367},[358,2273,462],{"class":363},[358,2275,465],{"class":367},[358,2277,2278],{"class":316,"line":397},[358,2279,2280],{"class":387},"    # Use host= in connect_args for Unix socket path; URL host is ignored\n",[358,2282,2283,2286],{"class":316,"line":403},[358,2284,2285],{"class":436},"    \"postgresql+asyncpg:\u002F\u002Fapp:secret@localhost\u002Forders\"",[358,2287,474],{"class":367},[358,2289,2290,2292,2294,2297],{"class":316,"line":409},[358,2291,481],{"class":480},[358,2293,462],{"class":363},[358,2295,2296],{"class":429},"15",[358,2298,474],{"class":367},[358,2300,2301,2303,2305,2307],{"class":316,"line":415},[358,2302,494],{"class":480},[358,2304,462],{"class":363},[358,2306,1824],{"class":429},[358,2308,474],{"class":367},[358,2310,2311,2313,2315,2317,2319],{"class":316,"line":421},[358,2312,520],{"class":480},[358,2314,462],{"class":363},[358,2316,798],{"class":429},[358,2318,1680],{"class":367},[358,2320,2321],{"class":387},"# Cloud SQL public IP idle timeout is 10 min\n",[358,2323,2324,2326,2328,2330],{"class":316,"line":426},[358,2325,537],{"class":480},[358,2327,462],{"class":363},[358,2329,542],{"class":429},[358,2331,474],{"class":367},[358,2333,2334,2336,2338],{"class":316,"line":440},[358,2335,1663],{"class":480},[358,2337,462],{"class":363},[358,2339,1395],{"class":367},[358,2341,2342,2345,2347,2350],{"class":316,"line":451},[358,2343,2344],{"class":436},"        \"host\"",[358,2346,1675],{"class":367},[358,2348,2349],{"class":436},"\"\u002Fcloudsql\u002Fmy-project:us-central1:my-instance\"",[358,2351,474],{"class":367},[358,2353,2354,2356,2358,2360,2362],{"class":316,"line":456},[358,2355,1898],{"class":436},[358,2357,1675],{"class":367},[358,2359,555],{"class":429},[358,2361,1680],{"class":367},[358,2363,2364],{"class":387},"# proxy handles TLS end-to-end\n",[358,2366,2367],{"class":316,"line":468},[358,2368,1730],{"class":367},[358,2370,2371,2373,2375,2377],{"class":316,"line":477},[358,2372,550],{"class":480},[358,2374,462],{"class":363},[358,2376,555],{"class":429},[358,2378,474],{"class":367},[358,2380,2381],{"class":316,"line":491},[358,2382,563],{"class":367},[52,2384,2386],{"id":2385},"azure-database-for-postgresql-flexible-server","Azure Database for PostgreSQL — Flexible Server",[14,2388,2389,2390,2393,2394,1750,2396,2398],{},"Azure Flexible Server enforces a 600-second idle connection timeout at the server level (configurable in server parameters, but often left at default). Set ",[18,2391,2392],{},"pool_recycle=300"," to recycle well within that window. Azure also supports PgBouncer as an integrated built-in proxy — when using it, apply the same ",[18,2395,38],{},[18,2397,1753],{}," pattern as for standalone PgBouncer.",[47,2400,2402],{"id":2401},"hybrid-architectures-migration-strategies","Hybrid Architectures & Migration Strategies",[52,2404,2406],{"id":2405},"migrating-from-a-single-host-connection-string-to-cloud-poolers","Migrating from a single-host connection string to cloud poolers",[14,2408,2409],{},"A common migration path is:",[1568,2411,2412,2420,2429,2432,2438],{},[1571,2413,2414,2415,2417,2418,45],{},"Start with direct RDS connection, ",[18,2416,1589],{},", conservative ",[18,2419,24],{},[1571,2421,2422,2423,132,2425,2428],{},"Enable ",[18,2424,34],{},[18,2426,2427],{},"pool_recycle=1800"," as an immediate stability improvement.",[1571,2430,2431],{},"Deploy RDS Proxy (or PgBouncer) in front of RDS.",[1571,2433,2434,2435,2437],{},"Reduce client-side ",[18,2436,24],{}," from 30–50 down to 3–10 per instance.",[1571,2439,2440,2441,2443],{},"Set ",[18,2442,1753],{}," if using asyncpg.",[14,2445,2446,2447,2449],{},"At step 4, watch ",[18,2448,1547],{}," to confirm server connections drop. If they do not, confirm the application's connection string is pointing at the proxy endpoint, not the direct RDS endpoint.",[14,2451,2452,2453,132,2457,45],{},"For detailed diagnosis and baseline configuration before adding a proxy layer, see ",[41,2454,2456],{"href":2455},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fconfiguring-async-engines-and-connection-pools\u002F","Configuring Async Engines and Connection Pools",[41,2458,2460],{"href":2459},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fhandling-connection-leaks-and-pool-exhaustion\u002F","Handling Connection Leaks and Pool Exhaustion",[52,2462,2464],{"id":2463},"multi-region-and-read-replica-routing","Multi-region and read-replica routing",[14,2466,2467,2468,2470,2471,2473],{},"In multi-region deployments, write traffic goes to the primary and read traffic fans out to replicas. Each region's application instances should use a replica local to that region to minimise latency. The pool budget calculation applies independently to each region: count the instances in that region, divide the regional replica's ",[18,2469,61],{},", and set ",[18,2472,24],{}," accordingly.",[104,2475,2477],{"className":352,"code":2476,"language":354,"meta":112,"style":112},"from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker\nfrom sqlalchemy import select\nimport os\n\nREGION = os.environ.get(\"AWS_REGION\", \"us-east-1\")\n\nREPLICA_URLS = {\n    \"us-east-1\": \"postgresql+asyncpg:\u002F\u002Fapp:secret@replica-use1.cluster.rds.amazonaws.com\u002Forders\",\n    \"eu-west-1\": \"postgresql+asyncpg:\u002F\u002Fapp:secret@replica-euw1.cluster.rds.amazonaws.com\u002Forders\",\n}\n\nread_engine = create_async_engine(\n    REPLICA_URLS[REGION],\n    pool_size=20,\n    max_overflow=10,\n    pool_recycle=1800,\n    pool_pre_ping=True,\n    echo=False,\n)\n\nReadSession = async_sessionmaker(read_engine, class_=AsyncSession, expire_on_commit=False)\n\n\nasync def get_tenant_invoice_total(session: AsyncSession, tenant_id: str) -> int:\n    from sqlalchemy import func\n    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n    import uuid\n\n    stmt = (\n        select(func.sum(Order.total_cents))\n        .where(Order.tenant_id == tenant_id)\n    )\n    result = await session.execute(stmt)\n    return result.scalar_one_or_none() or 0\n",[18,2478,2479,2489,2499,2506,2510,2530,2534,2544,2556,2568,2573,2577,2586,2599,2609,2619,2629,2639,2649,2653,2657,2681,2685,2689,2708,2720,2730,2736,2740,2749,2754,2763,2768,2778],{"__ignoreMap":112},[358,2480,2481,2483,2485,2487],{"class":316,"line":360},[358,2482,364],{"class":363},[358,2484,368],{"class":367},[358,2486,371],{"class":363},[358,2488,1958],{"class":367},[358,2490,2491,2493,2495,2497],{"class":316,"line":377},[358,2492,364],{"class":363},[358,2494,998],{"class":367},[358,2496,371],{"class":363},[358,2498,1003],{"class":367},[358,2500,2501,2503],{"class":316,"line":384},[358,2502,371],{"class":363},[358,2504,2505],{"class":367}," os\n",[358,2507,2508],{"class":316,"line":391},[358,2509,381],{"emptyLinePlaceholder":380},[358,2511,2512,2515,2517,2520,2523,2525,2528],{"class":316,"line":397},[358,2513,2514],{"class":429},"REGION",[358,2516,433],{"class":363},[358,2518,2519],{"class":367}," os.environ.get(",[358,2521,2522],{"class":436},"\"AWS_REGION\"",[358,2524,25],{"class":367},[358,2526,2527],{"class":436},"\"us-east-1\"",[358,2529,563],{"class":367},[358,2531,2532],{"class":316,"line":403},[358,2533,381],{"emptyLinePlaceholder":380},[358,2535,2536,2539,2541],{"class":316,"line":409},[358,2537,2538],{"class":429},"REPLICA_URLS",[358,2540,433],{"class":363},[358,2542,2543],{"class":367}," {\n",[358,2545,2546,2549,2551,2554],{"class":316,"line":415},[358,2547,2548],{"class":436},"    \"us-east-1\"",[358,2550,1675],{"class":367},[358,2552,2553],{"class":436},"\"postgresql+asyncpg:\u002F\u002Fapp:secret@replica-use1.cluster.rds.amazonaws.com\u002Forders\"",[358,2555,474],{"class":367},[358,2557,2558,2561,2563,2566],{"class":316,"line":421},[358,2559,2560],{"class":436},"    \"eu-west-1\"",[358,2562,1675],{"class":367},[358,2564,2565],{"class":436},"\"postgresql+asyncpg:\u002F\u002Fapp:secret@replica-euw1.cluster.rds.amazonaws.com\u002Forders\"",[358,2567,474],{"class":367},[358,2569,2570],{"class":316,"line":426},[358,2571,2572],{"class":367},"}\n",[358,2574,2575],{"class":316,"line":440},[358,2576,381],{"emptyLinePlaceholder":380},[358,2578,2579,2582,2584],{"class":316,"line":451},[358,2580,2581],{"class":367},"read_engine ",[358,2583,462],{"class":363},[358,2585,465],{"class":367},[358,2587,2588,2591,2594,2596],{"class":316,"line":456},[358,2589,2590],{"class":429},"    REPLICA_URLS",[358,2592,2593],{"class":367},"[",[358,2595,2514],{"class":429},[358,2597,2598],{"class":367},"],\n",[358,2600,2601,2603,2605,2607],{"class":316,"line":468},[358,2602,481],{"class":480},[358,2604,462],{"class":363},[358,2606,886],{"class":429},[358,2608,474],{"class":367},[358,2610,2611,2613,2615,2617],{"class":316,"line":477},[358,2612,494],{"class":480},[358,2614,462],{"class":363},[358,2616,897],{"class":429},[358,2618,474],{"class":367},[358,2620,2621,2623,2625,2627],{"class":316,"line":491},[358,2622,520],{"class":480},[358,2624,462],{"class":363},[358,2626,525],{"class":429},[358,2628,474],{"class":367},[358,2630,2631,2633,2635,2637],{"class":316,"line":504},[358,2632,537],{"class":480},[358,2634,462],{"class":363},[358,2636,542],{"class":429},[358,2638,474],{"class":367},[358,2640,2641,2643,2645,2647],{"class":316,"line":517},[358,2642,550],{"class":480},[358,2644,462],{"class":363},[358,2646,555],{"class":429},[358,2648,474],{"class":367},[358,2650,2651],{"class":316,"line":534},[358,2652,563],{"class":367},[358,2654,2655],{"class":316,"line":547},[358,2656,381],{"emptyLinePlaceholder":380},[358,2658,2659,2662,2664,2667,2669,2671,2673,2675,2677,2679],{"class":316,"line":560},[358,2660,2661],{"class":367},"ReadSession ",[358,2663,462],{"class":363},[358,2665,2666],{"class":367}," async_sessionmaker(read_engine, ",[358,2668,676],{"class":480},[358,2670,462],{"class":363},[358,2672,681],{"class":367},[358,2674,684],{"class":480},[358,2676,462],{"class":363},[358,2678,555],{"class":429},[358,2680,563],{"class":367},[358,2682,2683],{"class":316,"line":566},[358,2684,381],{"emptyLinePlaceholder":380},[358,2686,2687],{"class":316,"line":571},[358,2688,381],{"emptyLinePlaceholder":380},[358,2690,2691,2693,2695,2698,2700,2702,2704,2706],{"class":316,"line":581},[358,2692,1155],{"class":363},[358,2694,1158],{"class":363},[358,2696,2697],{"class":1038}," get_tenant_invoice_total",[358,2699,1164],{"class":367},[358,2701,1129],{"class":429},[358,2703,2126],{"class":367},[358,2705,1140],{"class":429},[358,2707,1303],{"class":367},[358,2709,2710,2713,2715,2717],{"class":316,"line":589},[358,2711,2712],{"class":363},"    from",[358,2714,998],{"class":367},[358,2716,371],{"class":363},[358,2718,2719],{"class":367}," func\n",[358,2721,2722,2724,2726,2728],{"class":316,"line":600},[358,2723,2712],{"class":363},[358,2725,1010],{"class":367},[358,2727,371],{"class":363},[358,2729,1015],{"class":367},[358,2731,2732,2734],{"class":316,"line":611},[358,2733,1313],{"class":363},[358,2735,1022],{"class":367},[358,2737,2738],{"class":316,"line":622},[358,2739,381],{"emptyLinePlaceholder":380},[358,2741,2742,2744,2746],{"class":316,"line":633},[358,2743,1184],{"class":367},[358,2745,462],{"class":363},[358,2747,2748],{"class":367}," (\n",[358,2750,2751],{"class":316,"line":644},[358,2752,2753],{"class":367},"        select(func.sum(Order.total_cents))\n",[358,2755,2756,2759,2761],{"class":316,"line":655},[358,2757,2758],{"class":367},"        .where(Order.tenant_id ",[358,2760,1192],{"class":363},[358,2762,1195],{"class":367},[358,2764,2765],{"class":316,"line":660},[358,2766,2767],{"class":367},"    )\n",[358,2769,2770,2772,2774,2776],{"class":316,"line":665},[358,2771,1200],{"class":367},[358,2773,462],{"class":363},[358,2775,1205],{"class":363},[358,2777,1208],{"class":367},[358,2779,2780,2782,2785,2788],{"class":316,"line":693},[358,2781,1213],{"class":363},[358,2783,2784],{"class":367}," result.scalar_one_or_none() ",[358,2786,2787],{"class":363},"or",[358,2789,2790],{"class":429}," 0\n",[47,2792,2794],{"id":2793},"production-pitfalls-anti-patterns","Production Pitfalls & Anti-Patterns",[2796,2797,2798,2820,2840,2852,2867,2875],"ul",{},[1571,2799,2800,2805,2806,2809,2810,2813,2814,2817,2818,45],{},[87,2801,827,2802,2804],{},[18,2803,24],{}," without accounting for all app instances",": A ",[18,2807,2808],{},"pool_size=30"," that works fine on one container causes ",[18,2811,2812],{},"FATAL: remaining connection slots are reserved for non-replication superuser connections"," when you scale to 10 containers. Always multiply ",[18,2815,2816],{},"pool_size + max_overflow"," by the maximum container\u002Fpod count and verify against ",[18,2819,61],{},[1571,2821,2822,2828,2829,2832,2833,2835,2836,2839],{},[87,2823,2824,2825,2827],{},"Forgetting ",[18,2826,1753],{}," behind PgBouncer",": The error ",[18,2830,2831],{},"asyncpg.exceptions.InvalidSQLStatementNameError: prepared statement \"__asyncpg_0\" does not exist"," appears under load, not during startup, because asyncpg defers statement caching. Adding ",[18,2834,1753],{}," to ",[18,2837,2838],{},"connect_args"," resolves it immediately.",[1571,2841,2842,2848,2849,2851],{},[87,2843,2844,2845,2847],{},"Using QueuePool with large ",[18,2846,24],{}," in Lambda",": Each Lambda invocation holding an idle pool exhausts RDS connections silently. The pool stays open for the life of the container, which can be hours. Switch to ",[18,2850,38],{}," and add an external proxy.",[1571,2853,2854,2860,2861,2864,2865,45],{},[87,2855,2856,2857,2859],{},"Omitting ",[18,2858,31],{}," on Azure Flexible Server",": Without recycling, connections idle for more than 600 seconds raise ",[18,2862,2863],{},"asyncpg.exceptions.ConnectionDoesNotExistError"," at checkout — not on the next query, but when the pool hands the stale socket to the coroutine. The fix is ",[18,2866,2392],{},[1571,2868,2869,2874],{},[87,2870,827,2871,2873],{},[18,2872,31],{}," below 60 seconds",": This forces TCP teardown and re-authentication on every minute boundary. For asyncpg with TLS and IAM token auth (RDS), a fresh connection involves a full TLS handshake plus token validation — 20–80 ms. Overly aggressive recycling adds observable p99 latency to all requests.",[1571,2876,2877,2882,2883,2886,2887,2889],{},[87,2878,2879,2880],{},"Ignoring the replica's own ",[18,2881,61],{},": Read replicas on smaller instance classes (e.g., ",[18,2884,2885],{},"db.t3.small",") have a much lower ",[18,2888,61],{}," than the primary. Routing all read traffic to an under-sized replica causes connection rejections for reads while writes to the primary succeed.",[47,2891,2893],{"id":2892},"frequently-asked-questions","Frequently Asked Questions",[14,2895,2896,2902,2903,2906,2907,2909,2910,2912],{},[87,2897,2898,2899,2901],{},"How do I find out the actual ",[18,2900,61],{}," for my RDS instance class?","\nRun ",[18,2904,2905],{},"SHOW max_connections;"," from any psql session connected to the instance, or check the ",[18,2908,61],{}," parameter in the RDS Parameter Group in the AWS Console. The RDS documentation table is approximate — the formula is ",[18,2911,69],{},", but the console value is authoritative.",[14,2914,2915,2923,2924,2926,2927,2929],{},[87,2916,2917,2918,144,2920,2922],{},"Should I use ",[18,2919,34],{},[18,2921,31],{}," on RDS?","\nBoth, at the values in the table above. ",[18,2925,31],{}," proactively prevents stale connections from accumulating in the pool; ",[18,2928,34],{}," catches the occasional connection that goes stale within the recycle window (e.g., after an RDS Multi-AZ failover). They are complementary and the combined overhead is negligible on private-IP cloud networks.",[14,2931,2932,2935,2936,2939,2940,2943,2944,2947],{},[87,2933,2934],{},"Can I share one async engine across multiple FastAPI workers on the same host?","\nNo. Each Uvicorn worker is a separate process with its own event loop. An ",[18,2937,2938],{},"AsyncEngine"," is not fork-safe and cannot be passed across ",[18,2941,2942],{},"fork()",". Initialize the engine inside each worker process — typically in the FastAPI lifespan or a ",[18,2945,2946],{},"@app.on_event(\"startup\")"," handler — and account for all worker processes in your pool budget calculation.",[14,2949,2950,2956,2957,2960,2961,2963,2964,2967,2968,2970],{},[87,2951,2952,2953,2955],{},"What happens when I deploy behind both RDS Proxy and keep a large ",[18,2954,24],{},"?","\nEach client connection to RDS Proxy counts against the proxy's ",[18,2958,2959],{},"MaxConnectionsPercent"," ceiling. A large ",[18,2962,24],{}," per container may saturate proxy capacity before database capacity, causing the proxy to reject new connections with ",[18,2965,2966],{},"FATAL: remaining connection slots are reserved",". Reduce ",[18,2969,24],{}," to 3–10 per instance when the proxy is in the path.",[14,2972,2973,2976,2977,2980,2981,2984,2985,2987,2988,2990,2991,2994,2995,2998,2999,3002],{},[87,2974,2975],{},"How do I test pool exhaustion before production?","\nUse ",[18,2978,2979],{},"asyncio.gather"," to launch ",[18,2982,2983],{},"pool_size + max_overflow + 1"," concurrent coroutines each holding a connection via ",[18,2986,147],{}," within a ",[18,2989,970],{}," block. The ",[18,2992,2993],{},"+1"," coroutine should raise ",[18,2996,2997],{},"sqlalchemy.exc.TimeoutError"," within ",[18,3000,3001],{},"pool_timeout"," seconds, confirming your limits are correctly set. Run this against a staging RDS instance, not a production database.",[47,3004,3006],{"id":3005},"related","Related",[2796,3008,3009,3016,3021,3026,3033],{},[1571,3010,3011,3015],{},[41,3012,3014],{"href":3013},"\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002Fsetting-pool-size-and-max-overflow-for-aws-rds\u002F","Setting Pool Size and max_overflow for AWS RDS"," — Worked formula and per-instance calculation for RDS and Aurora, with an error table.",[1571,3017,3018,3020],{},[41,3019,2456],{"href":2455}," — Baseline engine initialization, dialect strings, and pool parameter reference.",[1571,3022,3023,3025],{},[41,3024,2460],{"href":2459}," — Diagnosing and fixing pool exhaustion regardless of hosting environment.",[1571,3027,3028,3032],{},[41,3029,3031],{"href":3030},"\u002Fasync-engines-dialects-and-connection-pooling\u002Fdialect-specific-gotchas-and-driver-quirks\u002F","Dialect-Specific Gotchas and Driver Quirks"," — asyncpg and psycopg edge cases that surface specifically in cloud proxy configurations.",[1571,3034,3035,3037],{},[41,3036,44],{"href":43}," — Parent overview covering the full async engine and pooling architecture.",[3039,3040,3041],"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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":112,"searchDepth":377,"depth":377,"links":3043},[3044,3048,3055,3058,3065,3069,3070,3071],{"id":49,"depth":377,"text":50,"children":3045},[3046,3047],{"id":54,"depth":384,"text":55},{"id":124,"depth":384,"text":125},{"id":338,"depth":377,"text":339,"children":3049},[3050,3051,3053],{"id":342,"depth":384,"text":343},{"id":727,"depth":384,"text":3052},"pool_recycle below the provider idle timeout",{"id":833,"depth":384,"text":3054},"pool_pre_ping in cloud environments",{"id":963,"depth":377,"text":964,"children":3056},[3057],{"id":1225,"depth":384,"text":1226},{"id":1551,"depth":377,"text":1552,"children":3059},[3060,3061,3062,3063,3064],{"id":1555,"depth":384,"text":1556},{"id":1765,"depth":384,"text":1766},{"id":1935,"depth":384,"text":1936},{"id":2237,"depth":384,"text":2238},{"id":2385,"depth":384,"text":2386},{"id":2401,"depth":377,"text":2402,"children":3066},[3067,3068],{"id":2405,"depth":384,"text":2406},{"id":2463,"depth":384,"text":2464},{"id":2793,"depth":377,"text":2794},{"id":2892,"depth":377,"text":2893},{"id":3005,"depth":377,"text":3006},"Managing a SQLAlchemy async connection pool against a managed cloud Postgres service — AWS RDS, Aurora, GCP Cloud SQL, or Azure Database for PostgreSQL — demands more discipline than tuning against a self-hosted database. Cloud providers enforce hard connection limits that vary by instance class, interpose their own proxying infrastructure, and aggressively recycle idle TCP connections. Getting any single parameter wrong ripples into QueuePool limit reached errors, silent connection drops, or excess costs from over-provisioning. This guide covers every lever — pool_size, max_overflow, pool_recycle, pool_pre_ping, and NullPool — with concrete numbers and the reasoning behind them. For the architectural foundation of async engine initialization, see Async Engines, Dialects, and Connection Pooling.","md",{"date":3075},"2026-06-18","\u002Fasync-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases",{"title":5,"description":3072},"async-engines-dialects-and-connection-pooling\u002Ftuning-connection-pools-for-cloud-databases\u002Findex","6qhB2z0lz-Ufpsg8lfwO7qM1u99JDABvmX4emD24GIY",1781810028984]