[{"data":1,"prerenderedAt":2562},["ShallowReactive",2],{"page-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002F":3,"faq-schema-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002F":2541},{"id":4,"title":5,"body":6,"description":2531,"extension":2532,"meta":2533,"navigation":248,"path":2537,"seo":2538,"stem":2539,"__hash__":2540},"content\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002Findex.md","Zero-Downtime Deploys for Python APIs",{"type":7,"value":8,"toc":2518},"minimark",[9,13,23,26,29,34,37,40,171,173,177,180,193,818,836,948,950,954,967,970,1033,1048,1061,1162,1164,1168,1175,1191,1259,1518,1542,1635,1637,1641,1644,1652,2249,2264,2369,2371,2375,2378,2385,2388,2393,2417,2419,2423,2426,2428,2432,2438,2444,2450,2456,2462,2466,2471,2494,2499,2514],[10,11,5],"h1",{"id":12},"zero-downtime-deploys-for-python-apis",[14,15,16,17,22],"p",{},"Every deploy of a paid API is a moment where you can hand your customers a 502 and a reason to ask for a refund. Part of the ",[18,19,21],"a",{"href":20},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002F","Deploying APIs to Render or Vercel"," guide, this page resolves one decision: what you have to build into a Python service so that shipping four times a day costs your users exactly zero failed requests. The answer is four mechanisms working together — a readiness gate the platform can trust, graceful shutdown that drains in-flight work, schema migrations ordered so old and new code both run against the same database, and a rollback trigger that fires without a human in the loop.",[14,24,25],{},"None of this is platform magic. Render, Railway, and Fly all run rolling deploys by default, and all of them will happily roll a broken build straight into production if your app lies about being healthy. The platform's job is to shift traffic; your job is to tell it the truth about when to shift it.",[27,28],"hr",{},[30,31,33],"h2",{"id":32},"what-actually-breaks-during-a-deploy","What actually breaks during a deploy",[14,35,36],{},"A naive deploy is a stop-then-start: the old process gets killed, the new one boots, and every request that lands in between hits a closed socket. On a Python API that boot window is not trivial. Importing FastAPI, Pydantic models, and a SQLAlchemy metadata tree costs a second or two on a small instance; opening the database pool and warming the first connection costs more. Eight seconds of hard downtime per deploy, four deploys a day, is roughly thirty-two seconds of outage daily — enough to blow a 99.9% uptime commitment inside a month and enough to make webhook senders start retrying.",[14,38,39],{},"A rolling deploy fixes the arithmetic by overlapping: the new instance boots and proves itself healthy while the old one is still serving, and only then does the old one start refusing new work. The overlap window is where all the engineering lives. Get it wrong in either direction and you either cut requests mid-flight or leave two incompatible versions of your code fighting over the same tables.",[41,42,50,51,50,55,50,59,50,66,50,74,50,79,50,83,50,92,50,98,50,104,50,108,50,113,50,117,50,121,50,128,50,132,50,136,50,145,50,150,50,154,50,157,50,162,50,167],"svg",{"viewBox":43,"role":44,"ariaLabelledBy":45,"xmlns":48,"style":49},"0 0 720 270","img",[46,47],"zdd-timeline-t","zdd-timeline-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[52,53,54],"title",{"id":46},"Stop-and-start deploy versus rolling deploy timeline",[56,57,58],"desc",{"id":47},"A stop-then-start release leaves an eight second gap where requests fail, while a rolling release overlaps the old and new instances so every request is served.",[60,61],"rect",{"x":62,"y":62,"width":63,"height":64,"fill":65},"0","720","270","var(--c-surface)",[67,68,73],"text",{"x":69,"y":70,"fill":71,"style":72},"16","24","var(--c-text-muted)","font-size:12;font-family:var(--font-sans);","Request availability across one release",[67,75,78],{"x":69,"y":76,"fill":77,"style":72},"62","var(--c-text)","Stop, then",[67,80,82],{"x":69,"y":81,"fill":77,"style":72},"80","start",[60,84],{"x":85,"y":86,"width":85,"height":87,"rx":88,"fill":89,"stroke":90,"style":91},"160","46","30","4","var(--c-surface-alt)","var(--c-blue)","stroke-width:2;",[67,93,97],{"x":94,"y":95,"fill":77,"style":96},"240","66","text-anchor:middle;font-size:12;font-family:var(--font-sans);","v1 serving",[60,99],{"x":100,"y":86,"width":101,"height":87,"rx":88,"fill":65,"stroke":102,"style":103},"320","120","var(--c-coral)","stroke-width:2;stroke-dasharray:5 4;",[67,105,107],{"x":106,"y":95,"fill":77,"style":96},"380","502 \u002F 503",[60,109],{"x":110,"y":86,"width":111,"height":87,"rx":88,"fill":89,"stroke":112,"style":91},"440","260","var(--c-teal)",[67,114,116],{"x":115,"y":95,"fill":77,"style":96},"570","v2 serving",[67,118,120],{"x":106,"y":119,"fill":77,"style":96},"98","about 8 s of dropped traffic",[122,123],"line",{"x1":69,"y1":124,"x2":125,"y2":124,"stroke":126,"style":127},"116","704","var(--c-border)","stroke-width:1;",[67,129,131],{"x":69,"y":130,"fill":77,"style":72},"158","Rolling with",[67,133,135],{"x":69,"y":134,"fill":77,"style":72},"176","drain",[60,137],{"x":138,"y":139,"width":140,"height":141,"rx":142,"fill":143,"stroke":144,"style":103},"360","132","100","78","6","none","var(--c-yellow)",[60,146],{"x":85,"y":147,"width":148,"height":149,"rx":88,"fill":89,"stroke":90,"style":91},"140","300","26",[67,151,153],{"x":152,"y":130,"fill":77,"style":96},"310","v1 serving, then draining",[60,155],{"x":138,"y":134,"width":156,"height":149,"rx":88,"fill":89,"stroke":112,"style":91},"340",[67,158,161],{"x":159,"y":160,"fill":77,"style":96},"530","194","v2 ready, then serving",[67,163,166],{"x":164,"y":165,"fill":77,"style":96},"410","228","overlap window",[67,168,170],{"x":164,"y":169,"fill":71,"style":96},"248","zero failed requests",[27,172],{},[30,174,176],{"id":175},"health-checks-and-readiness-gates","Health checks and readiness gates",[14,178,179],{},"Ship two endpoints, not one. Liveness answers \"is this process alive enough to keep?\" and must never touch the database — if it does, a brief Postgres hiccup makes the platform kill and restart every healthy instance you own at exactly the moment the database is struggling. Readiness answers \"should the load balancer send me traffic right now?\" and is allowed to depend on the pool, the cache, and a boot-complete flag.",[14,181,182,183,187,188,192],{},"The readiness gate is what turns a rolling deploy from hopeful into safe. The new instance returns 503 from ",[184,185,186],"code",{},"\u002Freadyz"," until its ",[18,189,191],{"href":190},"\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002F","async SQLAlchemy engine"," has opened and verified a connection. The platform holds traffic back for exactly that long, then promotes it. Crucially the same flag flips back to false the moment shutdown starts, which is how you tell the router to stop sending you work before you stop being able to do it.",[194,195,200],"pre",{"className":196,"code":197,"language":198,"meta":199,"style":199},"language-python shiki shiki-themes github-light github-dark","# app\u002Fhealth.py\nimport asyncio\nimport os\nfrom contextlib import asynccontextmanager\n\nfrom fastapi import APIRouter, FastAPI, Response\nfrom sqlalchemy import text\nfrom sqlalchemy.ext.asyncio import create_async_engine\n\nDATABASE_URL = os.environ[\"DATABASE_URL\"]\nDRAIN_SECONDS = float(os.getenv(\"DRAIN_SECONDS\", \"5\"))\nRELEASE_SHA = os.getenv(\"RELEASE_SHA\", \"dev\")\n\nengine = create_async_engine(DATABASE_URL, pool_pre_ping=True)\nrouter = APIRouter()\n\n\nclass Gate:\n    \"\"\"Single source of truth for 'should I receive traffic?'.\"\"\"\n    ready = False\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    async with engine.connect() as conn:\n        await conn.execute(text(\"SELECT 1\"))\n    Gate.ready = True\n    yield\n    # Shutdown begins: fail readiness first, keep serving in-flight work.\n    Gate.ready = False\n    await asyncio.sleep(DRAIN_SECONDS)\n    await engine.dispose()\n\n\n@router.get(\"\u002Fhealthz\", include_in_schema=False)\nasync def healthz() -> dict[str, str]:\n    \"\"\"Liveness: process-only, never touches I\u002FO.\"\"\"\n    return {\"status\": \"alive\", \"release\": RELEASE_SHA}\n\n\n@router.get(\"\u002Freadyz\", include_in_schema=False)\nasync def readyz(response: Response) -> dict[str, str]:\n    \"\"\"Readiness: the routing decision.\"\"\"\n    if not Gate.ready:\n        response.status_code = 503\n        return {\"status\": \"draining\", \"release\": RELEASE_SHA}\n    return {\"status\": \"ready\", \"release\": RELEASE_SHA}\n\n\napp = FastAPI(lifespan=lifespan)\napp.include_router(router)\n","python","",[184,201,202,210,221,229,243,250,263,276,289,294,314,340,362,367,394,405,410,415,428,434,445,450,455,461,476,494,508,519,525,531,540,553,561,566,571,595,618,624,654,659,664,684,705,711,723,734,759,783,788,793,812],{"__ignoreMap":199},[203,204,206],"span",{"class":122,"line":205},1,[203,207,209],{"class":208},"sJ8bj","# app\u002Fhealth.py\n",[203,211,213,217],{"class":122,"line":212},2,[203,214,216],{"class":215},"szBVR","import",[203,218,220],{"class":219},"sVt8B"," asyncio\n",[203,222,224,226],{"class":122,"line":223},3,[203,225,216],{"class":215},[203,227,228],{"class":219}," os\n",[203,230,232,235,238,240],{"class":122,"line":231},4,[203,233,234],{"class":215},"from",[203,236,237],{"class":219}," contextlib ",[203,239,216],{"class":215},[203,241,242],{"class":219}," asynccontextmanager\n",[203,244,246],{"class":122,"line":245},5,[203,247,249],{"emptyLinePlaceholder":248},true,"\n",[203,251,253,255,258,260],{"class":122,"line":252},6,[203,254,234],{"class":215},[203,256,257],{"class":219}," fastapi ",[203,259,216],{"class":215},[203,261,262],{"class":219}," APIRouter, FastAPI, Response\n",[203,264,266,268,271,273],{"class":122,"line":265},7,[203,267,234],{"class":215},[203,269,270],{"class":219}," sqlalchemy ",[203,272,216],{"class":215},[203,274,275],{"class":219}," text\n",[203,277,279,281,284,286],{"class":122,"line":278},8,[203,280,234],{"class":215},[203,282,283],{"class":219}," sqlalchemy.ext.asyncio ",[203,285,216],{"class":215},[203,287,288],{"class":219}," create_async_engine\n",[203,290,292],{"class":122,"line":291},9,[203,293,249],{"emptyLinePlaceholder":248},[203,295,297,301,304,307,311],{"class":122,"line":296},10,[203,298,300],{"class":299},"sj4cs","DATABASE_URL",[203,302,303],{"class":215}," =",[203,305,306],{"class":219}," os.environ[",[203,308,310],{"class":309},"sZZnC","\"DATABASE_URL\"",[203,312,313],{"class":219},"]\n",[203,315,317,320,322,325,328,331,334,337],{"class":122,"line":316},11,[203,318,319],{"class":299},"DRAIN_SECONDS",[203,321,303],{"class":215},[203,323,324],{"class":299}," float",[203,326,327],{"class":219},"(os.getenv(",[203,329,330],{"class":309},"\"DRAIN_SECONDS\"",[203,332,333],{"class":219},", ",[203,335,336],{"class":309},"\"5\"",[203,338,339],{"class":219},"))\n",[203,341,343,346,348,351,354,356,359],{"class":122,"line":342},12,[203,344,345],{"class":299},"RELEASE_SHA",[203,347,303],{"class":215},[203,349,350],{"class":219}," os.getenv(",[203,352,353],{"class":309},"\"RELEASE_SHA\"",[203,355,333],{"class":219},[203,357,358],{"class":309},"\"dev\"",[203,360,361],{"class":219},")\n",[203,363,365],{"class":122,"line":364},13,[203,366,249],{"emptyLinePlaceholder":248},[203,368,370,373,376,379,381,383,387,389,392],{"class":122,"line":369},14,[203,371,372],{"class":219},"engine ",[203,374,375],{"class":215},"=",[203,377,378],{"class":219}," create_async_engine(",[203,380,300],{"class":299},[203,382,333],{"class":219},[203,384,386],{"class":385},"s4XuR","pool_pre_ping",[203,388,375],{"class":215},[203,390,391],{"class":299},"True",[203,393,361],{"class":219},[203,395,397,400,402],{"class":122,"line":396},15,[203,398,399],{"class":219},"router ",[203,401,375],{"class":215},[203,403,404],{"class":219}," APIRouter()\n",[203,406,408],{"class":122,"line":407},16,[203,409,249],{"emptyLinePlaceholder":248},[203,411,413],{"class":122,"line":412},17,[203,414,249],{"emptyLinePlaceholder":248},[203,416,418,421,425],{"class":122,"line":417},18,[203,419,420],{"class":215},"class",[203,422,424],{"class":423},"sScJk"," Gate",[203,426,427],{"class":219},":\n",[203,429,431],{"class":122,"line":430},19,[203,432,433],{"class":309},"    \"\"\"Single source of truth for 'should I receive traffic?'.\"\"\"\n",[203,435,437,440,442],{"class":122,"line":436},20,[203,438,439],{"class":219},"    ready ",[203,441,375],{"class":215},[203,443,444],{"class":299}," False\n",[203,446,448],{"class":122,"line":447},21,[203,449,249],{"emptyLinePlaceholder":248},[203,451,453],{"class":122,"line":452},22,[203,454,249],{"emptyLinePlaceholder":248},[203,456,458],{"class":122,"line":457},23,[203,459,460],{"class":423},"@asynccontextmanager\n",[203,462,464,467,470,473],{"class":122,"line":463},24,[203,465,466],{"class":215},"async",[203,468,469],{"class":215}," def",[203,471,472],{"class":423}," lifespan",[203,474,475],{"class":219},"(app: FastAPI):\n",[203,477,479,482,485,488,491],{"class":122,"line":478},25,[203,480,481],{"class":215},"    async",[203,483,484],{"class":215}," with",[203,486,487],{"class":219}," engine.connect() ",[203,489,490],{"class":215},"as",[203,492,493],{"class":219}," conn:\n",[203,495,497,500,503,506],{"class":122,"line":496},26,[203,498,499],{"class":215},"        await",[203,501,502],{"class":219}," conn.execute(text(",[203,504,505],{"class":309},"\"SELECT 1\"",[203,507,339],{"class":219},[203,509,511,514,516],{"class":122,"line":510},27,[203,512,513],{"class":219},"    Gate.ready ",[203,515,375],{"class":215},[203,517,518],{"class":299}," True\n",[203,520,522],{"class":122,"line":521},28,[203,523,524],{"class":215},"    yield\n",[203,526,528],{"class":122,"line":527},29,[203,529,530],{"class":208},"    # Shutdown begins: fail readiness first, keep serving in-flight work.\n",[203,532,534,536,538],{"class":122,"line":533},30,[203,535,513],{"class":219},[203,537,375],{"class":215},[203,539,444],{"class":299},[203,541,543,546,549,551],{"class":122,"line":542},31,[203,544,545],{"class":215},"    await",[203,547,548],{"class":219}," asyncio.sleep(",[203,550,319],{"class":299},[203,552,361],{"class":219},[203,554,556,558],{"class":122,"line":555},32,[203,557,545],{"class":215},[203,559,560],{"class":219}," engine.dispose()\n",[203,562,564],{"class":122,"line":563},33,[203,565,249],{"emptyLinePlaceholder":248},[203,567,569],{"class":122,"line":568},34,[203,570,249],{"emptyLinePlaceholder":248},[203,572,574,577,580,583,585,588,590,593],{"class":122,"line":573},35,[203,575,576],{"class":423},"@router.get",[203,578,579],{"class":219},"(",[203,581,582],{"class":309},"\"\u002Fhealthz\"",[203,584,333],{"class":219},[203,586,587],{"class":385},"include_in_schema",[203,589,375],{"class":215},[203,591,592],{"class":299},"False",[203,594,361],{"class":219},[203,596,598,600,602,605,608,611,613,615],{"class":122,"line":597},36,[203,599,466],{"class":215},[203,601,469],{"class":215},[203,603,604],{"class":423}," healthz",[203,606,607],{"class":219},"() -> dict[",[203,609,610],{"class":299},"str",[203,612,333],{"class":219},[203,614,610],{"class":299},[203,616,617],{"class":219},"]:\n",[203,619,621],{"class":122,"line":620},37,[203,622,623],{"class":309},"    \"\"\"Liveness: process-only, never touches I\u002FO.\"\"\"\n",[203,625,627,630,633,636,639,642,644,647,649,651],{"class":122,"line":626},38,[203,628,629],{"class":215},"    return",[203,631,632],{"class":219}," {",[203,634,635],{"class":309},"\"status\"",[203,637,638],{"class":219},": ",[203,640,641],{"class":309},"\"alive\"",[203,643,333],{"class":219},[203,645,646],{"class":309},"\"release\"",[203,648,638],{"class":219},[203,650,345],{"class":299},[203,652,653],{"class":219},"}\n",[203,655,657],{"class":122,"line":656},39,[203,658,249],{"emptyLinePlaceholder":248},[203,660,662],{"class":122,"line":661},40,[203,663,249],{"emptyLinePlaceholder":248},[203,665,667,669,671,674,676,678,680,682],{"class":122,"line":666},41,[203,668,576],{"class":423},[203,670,579],{"class":219},[203,672,673],{"class":309},"\"\u002Freadyz\"",[203,675,333],{"class":219},[203,677,587],{"class":385},[203,679,375],{"class":215},[203,681,592],{"class":299},[203,683,361],{"class":219},[203,685,687,689,691,694,697,699,701,703],{"class":122,"line":686},42,[203,688,466],{"class":215},[203,690,469],{"class":215},[203,692,693],{"class":423}," readyz",[203,695,696],{"class":219},"(response: Response) -> dict[",[203,698,610],{"class":299},[203,700,333],{"class":219},[203,702,610],{"class":299},[203,704,617],{"class":219},[203,706,708],{"class":122,"line":707},43,[203,709,710],{"class":309},"    \"\"\"Readiness: the routing decision.\"\"\"\n",[203,712,714,717,720],{"class":122,"line":713},44,[203,715,716],{"class":215},"    if",[203,718,719],{"class":215}," not",[203,721,722],{"class":219}," Gate.ready:\n",[203,724,726,729,731],{"class":122,"line":725},45,[203,727,728],{"class":219},"        response.status_code ",[203,730,375],{"class":215},[203,732,733],{"class":299}," 503\n",[203,735,737,740,742,744,746,749,751,753,755,757],{"class":122,"line":736},46,[203,738,739],{"class":215},"        return",[203,741,632],{"class":219},[203,743,635],{"class":309},[203,745,638],{"class":219},[203,747,748],{"class":309},"\"draining\"",[203,750,333],{"class":219},[203,752,646],{"class":309},[203,754,638],{"class":219},[203,756,345],{"class":299},[203,758,653],{"class":219},[203,760,762,764,766,768,770,773,775,777,779,781],{"class":122,"line":761},47,[203,763,629],{"class":215},[203,765,632],{"class":219},[203,767,635],{"class":309},[203,769,638],{"class":219},[203,771,772],{"class":309},"\"ready\"",[203,774,333],{"class":219},[203,776,646],{"class":309},[203,778,638],{"class":219},[203,780,345],{"class":299},[203,782,653],{"class":219},[203,784,786],{"class":122,"line":785},48,[203,787,249],{"emptyLinePlaceholder":248},[203,789,791],{"class":122,"line":790},49,[203,792,249],{"emptyLinePlaceholder":248},[203,794,796,799,801,804,807,809],{"class":122,"line":795},50,[203,797,798],{"class":219},"app ",[203,800,375],{"class":215},[203,802,803],{"class":219}," FastAPI(",[203,805,806],{"class":385},"lifespan",[203,808,375],{"class":215},[203,810,811],{"class":219},"lifespan)\n",[203,813,815],{"class":122,"line":814},51,[203,816,817],{"class":219},"app.include_router(router)\n",[14,819,820,821,823,824,827,828,831,832,835],{},"Point the platform's health check path at ",[184,822,186],{},", not ",[184,825,826],{},"\u002Fhealthz"," — Render's ",[184,829,830],{},"healthCheckPath",", Fly's ",[184,833,834],{},"[[http_service.checks]]",", and Railway's healthcheck field all gate traffic on it. Returning the release SHA is worth the two lines: curl the public URL in a loop during a deploy and watch the ratio of old to new SHAs shift, the cheapest possible proof that the overlap works.",[41,837,50,842,50,845,50,848,50,851,50,854,50,859,50,863,50,869,50,875,50,880,50,884,50,887,50,891,50,894,50,897,50,900,50,903,50,906,50,910,50,913,50,916,50,918,50,922,50,925,50,929,50,932,50,935,50,939,50,941,50,945],{"viewBox":838,"role":44,"ariaLabelledBy":839,"xmlns":48,"style":49},"0 0 720 230",[840,841],"zdd-lifecycle-t","zdd-lifecycle-d",[52,843,844],{"id":840},"Instance lifecycle as the readiness probe sees it",[56,846,847],{"id":841},"An instance moves from boot to warm-up to ready to draining to exited, and the load balancer routes traffic only while the readiness probe returns 200.",[60,849],{"x":62,"y":62,"width":63,"height":850,"fill":65},"230",[67,852,853],{"x":69,"y":70,"fill":71,"style":72},"What \u002Freadyz returns at each stage",[67,855,858],{"x":856,"y":857,"fill":77,"style":96},"363","58","probe passes",[67,860,862],{"x":861,"y":857,"fill":77,"style":96},"505","SIGTERM",[60,864],{"x":865,"y":866,"width":867,"height":868,"rx":142,"fill":89,"stroke":126,"style":91},"20","70","118","54",[67,870,874],{"x":871,"y":872,"fill":77,"style":873},"79","94","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Boot",[67,876,879],{"x":871,"y":877,"fill":71,"style":878},"112","text-anchor:middle;font-size:11;font-family:var(--font-sans);","no socket",[122,881],{"x1":882,"y1":883,"x2":130,"y2":883,"stroke":71,"style":91},"138","97",[60,885],{"x":886,"y":866,"width":867,"height":868,"rx":142,"fill":89,"stroke":90,"style":91},"162",[67,888,890],{"x":889,"y":872,"fill":77,"style":873},"221","Warm-up",[67,892,893],{"x":889,"y":877,"fill":71,"style":878},"503",[122,895],{"x1":896,"y1":883,"x2":148,"y2":883,"stroke":71,"style":91},"280",[60,898],{"x":899,"y":866,"width":867,"height":868,"rx":142,"fill":89,"stroke":112,"style":91},"304",[67,901,902],{"x":856,"y":872,"fill":77,"style":873},"Ready",[67,904,905],{"x":856,"y":877,"fill":71,"style":878},"200",[122,907],{"x1":908,"y1":883,"x2":909,"y2":883,"stroke":71,"style":91},"422","442",[60,911],{"x":912,"y":866,"width":867,"height":868,"rx":142,"fill":89,"stroke":144,"style":91},"446",[67,914,915],{"x":861,"y":872,"fill":77,"style":873},"Draining",[67,917,893],{"x":861,"y":877,"fill":71,"style":878},[122,919],{"x1":920,"y1":883,"x2":921,"y2":883,"stroke":71,"style":91},"564","584",[60,923],{"x":924,"y":866,"width":867,"height":868,"rx":142,"fill":89,"stroke":126,"style":91},"588",[67,926,928],{"x":927,"y":872,"fill":77,"style":873},"647","Exited",[67,930,931],{"x":927,"y":877,"fill":71,"style":878},"gone",[122,933],{"x1":899,"y1":934,"x2":908,"y2":934,"stroke":112,"style":91},"150",[122,936],{"x1":899,"y1":937,"x2":899,"y2":938,"stroke":112,"style":91},"144","156",[122,940],{"x1":908,"y1":937,"x2":908,"y2":938,"stroke":112,"style":91},[67,942,944],{"x":856,"y":943,"fill":77,"style":96},"174","new traffic routed only here",[67,946,947],{"x":856,"y":905,"fill":71,"style":96},"in-flight requests still finish while draining",[27,949],{},[30,951,953],{"id":952},"graceful-shutdown-draining-in-flight-requests","Graceful shutdown: draining in-flight requests",[14,955,956,957,959,960,963,964,966],{},"Flipping the readiness flag is only half of a clean exit. The platform sends ",[184,958,862],{},", waits a grace period, then sends ",[184,961,962],{},"SIGKILL",". Uvicorn handles ",[184,965,862],{}," by refusing new connections and waiting for open ones to complete, but it only waits as long as you let it — and the default grace period on most platforms is short enough to cut a slow report endpoint in half.",[14,968,969],{},"Three numbers have to be ordered correctly, and almost every broken drain I have debugged comes down to getting them backwards. Your longest realistic request must be shorter than Uvicorn's graceful-shutdown timeout, which must be shorter than the platform's kill grace period. Add the readiness-flip delay on top, because the router needs a probe cycle or two to notice you went unhealthy before it stops sending new work.",[194,971,975],{"className":972,"code":973,"language":974,"meta":199,"style":199},"language-dockerfile shiki shiki-themes github-light github-dark","# Dockerfile — the drain contract, expressed as flags\nENV DRAIN_SECONDS=5\nCMD [\"sh\", \"-c\", \"exec uvicorn app.health:app \\\n  --host 0.0.0.0 --port ${PORT:-8000} \\\n  --workers ${WEB_CONCURRENCY:-2} \\\n  --timeout-graceful-shutdown ${GRACEFUL_TIMEOUT:-25} \\\n  --timeout-keep-alive ${KEEPALIVE:-5}\"]\n","dockerfile",[184,976,977,982,990,1011,1016,1021,1026],{"__ignoreMap":199},[203,978,979],{"class":122,"line":205},[203,980,981],{"class":208},"# Dockerfile — the drain contract, expressed as flags\n",[203,983,984,987],{"class":122,"line":212},[203,985,986],{"class":215},"ENV",[203,988,989],{"class":219}," DRAIN_SECONDS=5\n",[203,991,992,995,998,1001,1003,1006,1008],{"class":122,"line":223},[203,993,994],{"class":215},"CMD",[203,996,997],{"class":219}," [",[203,999,1000],{"class":309},"\"sh\"",[203,1002,333],{"class":219},[203,1004,1005],{"class":309},"\"-c\"",[203,1007,333],{"class":219},[203,1009,1010],{"class":309},"\"exec uvicorn app.health:app \\\n",[203,1012,1013],{"class":122,"line":231},[203,1014,1015],{"class":309},"  --host 0.0.0.0 --port ${PORT:-8000} \\\n",[203,1017,1018],{"class":122,"line":245},[203,1019,1020],{"class":309},"  --workers ${WEB_CONCURRENCY:-2} \\\n",[203,1022,1023],{"class":122,"line":252},[203,1024,1025],{"class":309},"  --timeout-graceful-shutdown ${GRACEFUL_TIMEOUT:-25} \\\n",[203,1027,1028,1031],{"class":122,"line":265},[203,1029,1030],{"class":309},"  --timeout-keep-alive ${KEEPALIVE:-5}\"",[203,1032,313],{"class":219},[14,1034,1035,1036,1039,1040,1042,1043,1047],{},"The ",[184,1037,1038],{},"exec"," matters more than it looks: without it the shell becomes PID 1, swallows ",[184,1041,862],{},", and your carefully written lifespan shutdown never runs. Set the platform's own grace to something comfortably larger — 30 to 40 seconds against a 25-second Uvicorn timeout — so the process always exits on its own terms rather than being killed mid-response. If you run multiple workers, the same reasoning applies per worker; the worker-count decision itself is covered in ",[18,1044,1046],{"href":1045},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002Fuvicorn-vs-gunicorn-worker-configuration\u002F","Uvicorn vs Gunicorn worker configuration",".",[14,1049,1050,1051,1055,1056,1060],{},"Long-running work is the trap. If an endpoint holds a connection for ninety seconds rendering a PDF, no grace period saves you. Push that work to a queue and return a job id — ",[18,1052,1054],{"href":1053},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","background jobs with Celery"," exist precisely so your HTTP layer always drains in under thirty seconds. Webhook consumers need the same discipline; ",[18,1057,1059],{"href":1058},"\u002Fautomating-side-hustle-operations-with-apis\u002Fprocessing-webhooks-with-python\u002Fbuilding-an-idempotent-webhook-receiver\u002F","building an idempotent webhook receiver"," is what makes a mid-deploy retry harmless.",[41,1062,50,1067,50,1070,50,1073,50,1075,50,1078,50,1083,50,1085,50,1088,50,1090,50,1094,50,1098,50,1100,50,1102,50,1106,50,1110,50,1114,50,1118,50,1122,50,1127,50,1131,50,1134,50,1139,50,1143,50,1147,50,1151,50,1155,50,1159],{"viewBox":1063,"role":44,"ariaLabelledBy":1064,"xmlns":48,"style":49},"0 0 720 320",[1065,1066],"zdd-drain-t","zdd-drain-d",[52,1068,1069],{"id":1065},"Graceful shutdown sequence after SIGTERM",[56,1071,1072],{"id":1066},"The platform sends SIGTERM, the API process fails its readiness probe, the load balancer stops routing new requests, in-flight work finishes, and the process exits cleanly.",[60,1074],{"x":62,"y":62,"width":63,"height":100,"fill":65},[60,1076],{"x":87,"y":69,"width":85,"height":1077,"rx":142,"fill":89,"stroke":126,"style":91},"34",[67,1079,1082],{"x":1080,"y":1081,"fill":77,"style":96},"110","38","Platform",[60,1084],{"x":896,"y":69,"width":85,"height":1077,"rx":142,"fill":89,"stroke":90,"style":91},[67,1086,1087],{"x":138,"y":1081,"fill":77,"style":96},"API process",[60,1089],{"x":159,"y":69,"width":85,"height":1077,"rx":142,"fill":89,"stroke":112,"style":91},[67,1091,1093],{"x":1092,"y":1081,"fill":77,"style":96},"610","Load balancer",[122,1095],{"x1":1080,"y1":1096,"x2":1080,"y2":148,"stroke":126,"style":1097},"50","stroke-width:1;stroke-dasharray:4 4;",[122,1099],{"x1":138,"y1":1096,"x2":138,"y2":148,"stroke":126,"style":1097},[122,1101],{"x1":1092,"y1":1096,"x2":1092,"y2":148,"stroke":126,"style":1097},[67,1103,862],{"x":1104,"y":1105,"fill":77,"style":96},"235","82",[122,1107],{"x1":1080,"y1":1108,"x2":1109,"y2":1108,"stroke":102,"style":91},"92","356",[1111,1112],"polygon",{"points":1113,"fill":102},"360,92 350,87 350,97",[60,1115],{"x":1116,"y":877,"width":1117,"height":149,"rx":88,"fill":89,"stroke":90,"style":91},"370","215",[67,1119,1121],{"x":106,"y":1120,"fill":77,"style":72},"130","Gate.ready = False",[67,1123,1126],{"x":1124,"y":1125,"fill":77,"style":96},"487","164","GET \u002Freadyz gives 503",[122,1128],{"x1":1129,"y1":134,"x2":1130,"y2":134,"stroke":71,"style":91},"606","364",[1111,1132],{"points":1133,"fill":71},"360,176 370,171 370,181",[60,1135],{"x":1136,"y":1137,"width":1138,"height":149,"rx":88,"fill":89,"stroke":112,"style":91},"430","196","170",[67,1140,1142],{"x":110,"y":1141,"fill":77,"style":72},"214","stop new routing",[60,1144],{"x":1116,"y":1145,"width":1146,"height":149,"rx":88,"fill":89,"stroke":144,"style":91},"236","220",[67,1148,1150],{"x":106,"y":1149,"fill":77,"style":72},"254","drain in-flight, max 25 s",[67,1152,1154],{"x":1104,"y":1153,"fill":77,"style":96},"282","exit 0, before SIGKILL",[122,1156],{"x1":138,"y1":1157,"x2":1158,"y2":1157,"stroke":112,"style":91},"292","114",[1111,1160],{"points":1161,"fill":112},"110,292 120,287 120,297",[27,1163],{},[30,1165,1167],{"id":1166},"migration-ordering-expand-then-contract","Migration ordering: expand, then contract",[14,1169,1170,1171,1174],{},"Rolling deploys guarantee that old code and new code run at the same time against one database, usually for thirty to ninety seconds. Any migration that is not backwards compatible turns that overlap into an outage: rename a column and every request still handled by the old instance raises ",[184,1172,1173],{},"UndefinedColumn",". The rule is that a single deploy may never contain both a schema change and the code that requires it.",[14,1176,1177,1178,1182,1183,1186,1187,1190],{},"Split every breaking change into three releases. ",[1179,1180,1181],"strong",{},"Expand"," adds the new structure while leaving the old one intact — nullable columns, new tables, new indexes built concurrently. ",[1179,1184,1185],{},"Migrate"," ships code that writes both shapes and reads the new one, plus a backfill for existing rows. ",[1179,1188,1189],{},"Contract"," removes the old column, days later, once no running instance references it. Each step is individually reversible, which is the property that makes an automatic rollback safe.",[1192,1193,1194,1213],"table",{},[1195,1196,1197],"thead",{},[1198,1199,1200,1204,1207,1210],"tr",{},[1201,1202,1203],"th",{},"Step",[1201,1205,1206],{},"Schema change",[1201,1208,1209],{},"Code that runs",[1201,1211,1212],{},"Reversible",[1214,1215,1216,1231,1245],"tbody",{},[1198,1217,1218,1222,1225,1228],{},[1219,1220,1221],"td",{},"1. Expand",[1219,1223,1224],{},"Add nullable column",[1219,1226,1227],{},"v1 and v2 both fine",[1219,1229,1230],{},"Yes, drop it",[1198,1232,1233,1236,1239,1242],{},[1219,1234,1235],{},"2. Migrate",[1219,1237,1238],{},"Backfill rows",[1219,1240,1241],{},"v2 dual-writes, reads new",[1219,1243,1244],{},"Yes, v1 still works",[1198,1246,1247,1250,1253,1256],{},[1219,1248,1249],{},"3. Contract",[1219,1251,1252],{},"Drop old column",[1219,1254,1255],{},"v2 only",[1219,1257,1258],{},"No, plan it",[194,1260,1262],{"className":196,"code":1261,"language":198,"meta":199,"style":199},"\"\"\"expand: add plan_code beside plan_id\n\nRevision ID: 20260723_expand_plan_code\n\"\"\"\nimport sqlalchemy as sa\nfrom alembic import op\n\nrevision = \"20260723_expand_plan_code\"\ndown_revision = \"20260702_usage_events\"\n\n\ndef upgrade() -> None:\n    # Nullable: existing rows stay valid, v1 code keeps inserting without it.\n    op.add_column(\"subscriptions\", sa.Column(\"plan_code\", sa.Text(), nullable=True))\n\n    # CONCURRENTLY cannot run inside a transaction; Alembic gives us an escape hatch.\n    with op.get_context().autocommit_block():\n        op.create_index(\n            \"ix_subscriptions_plan_code\",\n            \"subscriptions\",\n            [\"plan_code\"],\n            unique=False,\n            postgresql_concurrently=True,\n        )\n\n\ndef downgrade() -> None:\n    with op.get_context().autocommit_block():\n        op.drop_index(\"ix_subscriptions_plan_code\", postgresql_concurrently=True)\n    op.drop_column(\"subscriptions\", \"plan_code\")\n",[184,1263,1264,1269,1273,1278,1283,1294,1306,1310,1320,1330,1334,1338,1354,1359,1385,1389,1394,1402,1407,1415,1422,1432,1443,1454,1459,1463,1467,1480,1486,1505],{"__ignoreMap":199},[203,1265,1266],{"class":122,"line":205},[203,1267,1268],{"class":309},"\"\"\"expand: add plan_code beside plan_id\n",[203,1270,1271],{"class":122,"line":212},[203,1272,249],{"emptyLinePlaceholder":248},[203,1274,1275],{"class":122,"line":223},[203,1276,1277],{"class":309},"Revision ID: 20260723_expand_plan_code\n",[203,1279,1280],{"class":122,"line":231},[203,1281,1282],{"class":309},"\"\"\"\n",[203,1284,1285,1287,1289,1291],{"class":122,"line":245},[203,1286,216],{"class":215},[203,1288,270],{"class":219},[203,1290,490],{"class":215},[203,1292,1293],{"class":219}," sa\n",[203,1295,1296,1298,1301,1303],{"class":122,"line":252},[203,1297,234],{"class":215},[203,1299,1300],{"class":219}," alembic ",[203,1302,216],{"class":215},[203,1304,1305],{"class":219}," op\n",[203,1307,1308],{"class":122,"line":265},[203,1309,249],{"emptyLinePlaceholder":248},[203,1311,1312,1315,1317],{"class":122,"line":278},[203,1313,1314],{"class":219},"revision ",[203,1316,375],{"class":215},[203,1318,1319],{"class":309}," \"20260723_expand_plan_code\"\n",[203,1321,1322,1325,1327],{"class":122,"line":291},[203,1323,1324],{"class":219},"down_revision ",[203,1326,375],{"class":215},[203,1328,1329],{"class":309}," \"20260702_usage_events\"\n",[203,1331,1332],{"class":122,"line":296},[203,1333,249],{"emptyLinePlaceholder":248},[203,1335,1336],{"class":122,"line":316},[203,1337,249],{"emptyLinePlaceholder":248},[203,1339,1340,1343,1346,1349,1352],{"class":122,"line":342},[203,1341,1342],{"class":215},"def",[203,1344,1345],{"class":423}," upgrade",[203,1347,1348],{"class":219},"() -> ",[203,1350,1351],{"class":299},"None",[203,1353,427],{"class":219},[203,1355,1356],{"class":122,"line":364},[203,1357,1358],{"class":208},"    # Nullable: existing rows stay valid, v1 code keeps inserting without it.\n",[203,1360,1361,1364,1367,1370,1373,1376,1379,1381,1383],{"class":122,"line":369},[203,1362,1363],{"class":219},"    op.add_column(",[203,1365,1366],{"class":309},"\"subscriptions\"",[203,1368,1369],{"class":219},", sa.Column(",[203,1371,1372],{"class":309},"\"plan_code\"",[203,1374,1375],{"class":219},", sa.Text(), ",[203,1377,1378],{"class":385},"nullable",[203,1380,375],{"class":215},[203,1382,391],{"class":299},[203,1384,339],{"class":219},[203,1386,1387],{"class":122,"line":396},[203,1388,249],{"emptyLinePlaceholder":248},[203,1390,1391],{"class":122,"line":407},[203,1392,1393],{"class":208},"    # CONCURRENTLY cannot run inside a transaction; Alembic gives us an escape hatch.\n",[203,1395,1396,1399],{"class":122,"line":412},[203,1397,1398],{"class":215},"    with",[203,1400,1401],{"class":219}," op.get_context().autocommit_block():\n",[203,1403,1404],{"class":122,"line":417},[203,1405,1406],{"class":219},"        op.create_index(\n",[203,1408,1409,1412],{"class":122,"line":430},[203,1410,1411],{"class":309},"            \"ix_subscriptions_plan_code\"",[203,1413,1414],{"class":219},",\n",[203,1416,1417,1420],{"class":122,"line":436},[203,1418,1419],{"class":309},"            \"subscriptions\"",[203,1421,1414],{"class":219},[203,1423,1424,1427,1429],{"class":122,"line":447},[203,1425,1426],{"class":219},"            [",[203,1428,1372],{"class":309},[203,1430,1431],{"class":219},"],\n",[203,1433,1434,1437,1439,1441],{"class":122,"line":452},[203,1435,1436],{"class":385},"            unique",[203,1438,375],{"class":215},[203,1440,592],{"class":299},[203,1442,1414],{"class":219},[203,1444,1445,1448,1450,1452],{"class":122,"line":457},[203,1446,1447],{"class":385},"            postgresql_concurrently",[203,1449,375],{"class":215},[203,1451,391],{"class":299},[203,1453,1414],{"class":219},[203,1455,1456],{"class":122,"line":463},[203,1457,1458],{"class":219},"        )\n",[203,1460,1461],{"class":122,"line":478},[203,1462,249],{"emptyLinePlaceholder":248},[203,1464,1465],{"class":122,"line":496},[203,1466,249],{"emptyLinePlaceholder":248},[203,1468,1469,1471,1474,1476,1478],{"class":122,"line":510},[203,1470,1342],{"class":215},[203,1472,1473],{"class":423}," downgrade",[203,1475,1348],{"class":219},[203,1477,1351],{"class":299},[203,1479,427],{"class":219},[203,1481,1482,1484],{"class":122,"line":521},[203,1483,1398],{"class":215},[203,1485,1401],{"class":219},[203,1487,1488,1491,1494,1496,1499,1501,1503],{"class":122,"line":527},[203,1489,1490],{"class":219},"        op.drop_index(",[203,1492,1493],{"class":309},"\"ix_subscriptions_plan_code\"",[203,1495,333],{"class":219},[203,1497,1498],{"class":385},"postgresql_concurrently",[203,1500,375],{"class":215},[203,1502,391],{"class":299},[203,1504,361],{"class":219},[203,1506,1507,1510,1512,1514,1516],{"class":122,"line":533},[203,1508,1509],{"class":219},"    op.drop_column(",[203,1511,1366],{"class":309},[203,1513,333],{"class":219},[203,1515,1372],{"class":309},[203,1517,361],{"class":219},[14,1519,1520,1521,1524,1525,1528,1529,1532,1533,1537,1538,1047],{},"Two Postgres details decide whether this is invisible or catastrophic. Building an index without ",[184,1522,1523],{},"CONCURRENTLY"," takes a lock that blocks writes for the whole build — minutes on a table with millions of usage rows. And any ",[184,1526,1527],{},"ALTER TABLE"," waiting behind a long transaction queues every later query behind it too, so set a short ",[184,1530,1531],{},"lock_timeout"," on the migration connection specifically and retry rather than letting one statement freeze the API. If releases already cause pool pressure, ",[18,1534,1536],{"href":1535},"\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Ffixing-connection-pool-exhaustion\u002F","fixing connection pool exhaustion"," is the prerequisite read. The same expand-contract discipline applies to your public response bodies, which is the subject of ",[18,1539,1541],{"href":1540},"\u002Fscaling-and-operating-production-python-apis\u002Fversioning-and-evolving-public-apis\u002Fdeprecating-an-api-endpoint-without-breaking-customers\u002F","deprecating an endpoint without breaking customers",[41,1543,50,1548,50,1551,50,1554,50,1556,50,1559,50,1562,50,1566,50,1571,50,1574,50,1578,50,1582,50,1585,50,1589,50,1592,50,1594,50,1596,50,1599,50,1601,50,1604,50,1607,50,1611,50,1614,50,1616,50,1618,50,1621,50,1624,50,1628,50,1632],{"viewBox":1544,"role":44,"ariaLabelledBy":1545,"xmlns":48,"style":49},"0 0 720 280",[1546,1547],"zdd-expand-t","zdd-expand-d",[52,1549,1550],{"id":1546},"Expand, migrate, and contract release ordering",[56,1552,1553],{"id":1547},"Three separate releases: expand adds a nullable column, migrate backfills and switches reads, and contract drops the old column once no old code remains.",[60,1555],{"x":62,"y":62,"width":63,"height":896,"fill":65},[67,1557,1558],{"x":1077,"y":87,"fill":71,"style":72},"Release",[67,1560,1561],{"x":850,"y":87,"fill":71,"style":72},"Schema state",[67,1563,1565],{"x":1564,"y":87,"fill":71,"style":72},"500","Code versions it serves",[60,1567],{"x":865,"y":1568,"width":1569,"height":76,"rx":1570,"fill":89,"stroke":126,"style":127},"44","680","8",[60,1572],{"x":865,"y":1568,"width":1573,"height":76,"fill":90},"7",[67,1575,1221],{"x":1576,"y":866,"fill":77,"style":1577},"42","font-size:13;font-family:var(--font-sans);",[67,1579,1581],{"x":1576,"y":1108,"fill":71,"style":1580},"font-size:11;font-family:var(--font-sans);","Monday",[67,1583,1584],{"x":850,"y":866,"fill":77,"style":72},"old column kept,",[67,1586,1588],{"x":850,"y":1587,"fill":77,"style":72},"90","new nullable column added",[67,1590,1591],{"x":1564,"y":81,"fill":77,"style":72},"v1 and v2 both work",[60,1593],{"x":865,"y":124,"width":1569,"height":76,"rx":1570,"fill":89,"stroke":126,"style":127},[60,1595],{"x":865,"y":124,"width":1573,"height":76,"fill":112},[67,1597,1235],{"x":1576,"y":1598,"fill":77,"style":1577},"142",[67,1600,1581],{"x":1576,"y":1125,"fill":71,"style":1580},[67,1602,1603],{"x":850,"y":1598,"fill":77,"style":72},"rows backfilled,",[67,1605,1606],{"x":850,"y":886,"fill":77,"style":72},"v2 writes both columns",[67,1608,1610],{"x":1564,"y":1609,"fill":77,"style":72},"152","v2 reads new, v1 safe",[60,1612],{"x":865,"y":1613,"width":1569,"height":76,"rx":1570,"fill":89,"stroke":126,"style":127},"188",[60,1615],{"x":865,"y":1613,"width":1573,"height":76,"fill":102},[67,1617,1249],{"x":1576,"y":1141,"fill":77,"style":1577},[67,1619,1620],{"x":1576,"y":1145,"fill":71,"style":1580},"Thursday",[67,1622,1623],{"x":850,"y":1141,"fill":77,"style":72},"old column dropped,",[67,1625,1627],{"x":850,"y":1626,"fill":77,"style":72},"234","index cleaned up",[67,1629,1631],{"x":1564,"y":1630,"fill":77,"style":72},"224","v2 only, no rollback",[67,1633,1634],{"x":865,"y":64,"fill":71,"style":72},"Steps 1 and 2 are reversible; wait days before step 3.",[27,1636],{},[30,1638,1640],{"id":1639},"rollback-triggers-that-fire-without-you","Rollback triggers that fire without you",[14,1642,1643],{},"A deploy is not finished when the platform says \"live\". It is finished when the new release has survived a watch window. Define that window in code and let a script decide, because at 11pm on a Friday your judgement is the least reliable component in the system.",[14,1645,1646,1647,1651],{},"Three signals are enough for a small API: the 5xx rate over a rolling minute, p95 latency against the pre-deploy baseline, and readiness flapping — an instance oscillating between 200 and 503 is a boot loop dressed up as a rolling deploy. Any one tripping should roll back automatically and page you afterwards. ",[18,1648,1650],{"href":1649},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002Fstructured-logging-with-structlog\u002F","Structured logging with structlog"," is the fastest route to per-release error rates.",[194,1653,1655],{"className":196,"code":1654,"language":198,"meta":199,"style":199},"# scripts\u002Fwatch_release.py — run right after the deploy finishes\nimport asyncio\nimport os\nimport sys\n\nimport httpx\n\nBASE_URL = os.environ[\"DEPLOY_BASE_URL\"]\nRELEASE_SHA = os.environ[\"RELEASE_SHA\"]\nWINDOW_SECONDS = int(os.getenv(\"WATCH_WINDOW_SECONDS\", \"600\"))\nMAX_ERROR_RATE = float(os.getenv(\"MAX_ERROR_RATE\", \"0.01\"))\nP95_BUDGET_MS = float(os.getenv(\"P95_BUDGET_MS\", \"800\"))\n\n\nasync def sample(client: httpx.AsyncClient) -> tuple[float, float, str]:\n    \"\"\"Read the app's own metrics endpoint for the current release.\"\"\"\n    resp = await client.get(\"\u002Finternal\u002Fmetrics\", params={\"release\": RELEASE_SHA})\n    resp.raise_for_status()\n    body = resp.json()\n    return float(body[\"error_rate\"]), float(body[\"p95_ms\"]), str(body[\"release\"])\n\n\nasync def verdict() -> str:\n    deadline = asyncio.get_running_loop().time() + WINDOW_SECONDS\n    async with httpx.AsyncClient(base_url=BASE_URL, timeout=10.0) as client:\n        while asyncio.get_running_loop().time() \u003C deadline:\n            error_rate, p95_ms, live_sha = await sample(client)\n            if live_sha != RELEASE_SHA:\n                return \"flapping\"\n            if error_rate > MAX_ERROR_RATE:\n                return \"errors\"\n            if p95_ms > P95_BUDGET_MS:\n                return \"latency\"\n            await asyncio.sleep(15)\n    return \"healthy\"\n\n\nasync def main() -> int:\n    match await verdict():\n        case \"healthy\":\n            print(f\"release {RELEASE_SHA} promoted\")\n            return 0\n        case reason:\n            print(f\"rolling back {RELEASE_SHA}: {reason}\", file=sys.stderr)\n            return 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(asyncio.run(main()))\n",[184,1656,1657,1662,1668,1674,1681,1685,1692,1696,1710,1722,1744,1765,1786,1790,1794,1819,1824,1859,1864,1874,1907,1911,1915,1930,1946,1980,1993,2005,2021,2029,2044,2051,2065,2072,2084,2091,2095,2099,2115,2125,2135,2156,2164,2171,2207,2214,2218,2222,2238],{"__ignoreMap":199},[203,1658,1659],{"class":122,"line":205},[203,1660,1661],{"class":208},"# scripts\u002Fwatch_release.py — run right after the deploy finishes\n",[203,1663,1664,1666],{"class":122,"line":212},[203,1665,216],{"class":215},[203,1667,220],{"class":219},[203,1669,1670,1672],{"class":122,"line":223},[203,1671,216],{"class":215},[203,1673,228],{"class":219},[203,1675,1676,1678],{"class":122,"line":231},[203,1677,216],{"class":215},[203,1679,1680],{"class":219}," sys\n",[203,1682,1683],{"class":122,"line":245},[203,1684,249],{"emptyLinePlaceholder":248},[203,1686,1687,1689],{"class":122,"line":252},[203,1688,216],{"class":215},[203,1690,1691],{"class":219}," httpx\n",[203,1693,1694],{"class":122,"line":265},[203,1695,249],{"emptyLinePlaceholder":248},[203,1697,1698,1701,1703,1705,1708],{"class":122,"line":278},[203,1699,1700],{"class":299},"BASE_URL",[203,1702,303],{"class":215},[203,1704,306],{"class":219},[203,1706,1707],{"class":309},"\"DEPLOY_BASE_URL\"",[203,1709,313],{"class":219},[203,1711,1712,1714,1716,1718,1720],{"class":122,"line":291},[203,1713,345],{"class":299},[203,1715,303],{"class":215},[203,1717,306],{"class":219},[203,1719,353],{"class":309},[203,1721,313],{"class":219},[203,1723,1724,1727,1729,1732,1734,1737,1739,1742],{"class":122,"line":296},[203,1725,1726],{"class":299},"WINDOW_SECONDS",[203,1728,303],{"class":215},[203,1730,1731],{"class":299}," int",[203,1733,327],{"class":219},[203,1735,1736],{"class":309},"\"WATCH_WINDOW_SECONDS\"",[203,1738,333],{"class":219},[203,1740,1741],{"class":309},"\"600\"",[203,1743,339],{"class":219},[203,1745,1746,1749,1751,1753,1755,1758,1760,1763],{"class":122,"line":316},[203,1747,1748],{"class":299},"MAX_ERROR_RATE",[203,1750,303],{"class":215},[203,1752,324],{"class":299},[203,1754,327],{"class":219},[203,1756,1757],{"class":309},"\"MAX_ERROR_RATE\"",[203,1759,333],{"class":219},[203,1761,1762],{"class":309},"\"0.01\"",[203,1764,339],{"class":219},[203,1766,1767,1770,1772,1774,1776,1779,1781,1784],{"class":122,"line":342},[203,1768,1769],{"class":299},"P95_BUDGET_MS",[203,1771,303],{"class":215},[203,1773,324],{"class":299},[203,1775,327],{"class":219},[203,1777,1778],{"class":309},"\"P95_BUDGET_MS\"",[203,1780,333],{"class":219},[203,1782,1783],{"class":309},"\"800\"",[203,1785,339],{"class":219},[203,1787,1788],{"class":122,"line":364},[203,1789,249],{"emptyLinePlaceholder":248},[203,1791,1792],{"class":122,"line":369},[203,1793,249],{"emptyLinePlaceholder":248},[203,1795,1796,1798,1800,1803,1806,1809,1811,1813,1815,1817],{"class":122,"line":396},[203,1797,466],{"class":215},[203,1799,469],{"class":215},[203,1801,1802],{"class":423}," sample",[203,1804,1805],{"class":219},"(client: httpx.AsyncClient) -> tuple[",[203,1807,1808],{"class":299},"float",[203,1810,333],{"class":219},[203,1812,1808],{"class":299},[203,1814,333],{"class":219},[203,1816,610],{"class":299},[203,1818,617],{"class":219},[203,1820,1821],{"class":122,"line":407},[203,1822,1823],{"class":309},"    \"\"\"Read the app's own metrics endpoint for the current release.\"\"\"\n",[203,1825,1826,1829,1831,1834,1837,1840,1842,1845,1847,1850,1852,1854,1856],{"class":122,"line":412},[203,1827,1828],{"class":219},"    resp ",[203,1830,375],{"class":215},[203,1832,1833],{"class":215}," await",[203,1835,1836],{"class":219}," client.get(",[203,1838,1839],{"class":309},"\"\u002Finternal\u002Fmetrics\"",[203,1841,333],{"class":219},[203,1843,1844],{"class":385},"params",[203,1846,375],{"class":215},[203,1848,1849],{"class":219},"{",[203,1851,646],{"class":309},[203,1853,638],{"class":219},[203,1855,345],{"class":299},[203,1857,1858],{"class":219},"})\n",[203,1860,1861],{"class":122,"line":417},[203,1862,1863],{"class":219},"    resp.raise_for_status()\n",[203,1865,1866,1869,1871],{"class":122,"line":430},[203,1867,1868],{"class":219},"    body ",[203,1870,375],{"class":215},[203,1872,1873],{"class":219}," resp.json()\n",[203,1875,1876,1878,1880,1883,1886,1889,1891,1893,1896,1898,1900,1902,1904],{"class":122,"line":436},[203,1877,629],{"class":215},[203,1879,324],{"class":299},[203,1881,1882],{"class":219},"(body[",[203,1884,1885],{"class":309},"\"error_rate\"",[203,1887,1888],{"class":219},"]), ",[203,1890,1808],{"class":299},[203,1892,1882],{"class":219},[203,1894,1895],{"class":309},"\"p95_ms\"",[203,1897,1888],{"class":219},[203,1899,610],{"class":299},[203,1901,1882],{"class":219},[203,1903,646],{"class":309},[203,1905,1906],{"class":219},"])\n",[203,1908,1909],{"class":122,"line":447},[203,1910,249],{"emptyLinePlaceholder":248},[203,1912,1913],{"class":122,"line":452},[203,1914,249],{"emptyLinePlaceholder":248},[203,1916,1917,1919,1921,1924,1926,1928],{"class":122,"line":457},[203,1918,466],{"class":215},[203,1920,469],{"class":215},[203,1922,1923],{"class":423}," verdict",[203,1925,1348],{"class":219},[203,1927,610],{"class":299},[203,1929,427],{"class":219},[203,1931,1932,1935,1937,1940,1943],{"class":122,"line":463},[203,1933,1934],{"class":219},"    deadline ",[203,1936,375],{"class":215},[203,1938,1939],{"class":219}," asyncio.get_running_loop().time() ",[203,1941,1942],{"class":215},"+",[203,1944,1945],{"class":299}," WINDOW_SECONDS\n",[203,1947,1948,1950,1952,1955,1958,1960,1962,1964,1967,1969,1972,1975,1977],{"class":122,"line":478},[203,1949,481],{"class":215},[203,1951,484],{"class":215},[203,1953,1954],{"class":219}," httpx.AsyncClient(",[203,1956,1957],{"class":385},"base_url",[203,1959,375],{"class":215},[203,1961,1700],{"class":299},[203,1963,333],{"class":219},[203,1965,1966],{"class":385},"timeout",[203,1968,375],{"class":215},[203,1970,1971],{"class":299},"10.0",[203,1973,1974],{"class":219},") ",[203,1976,490],{"class":215},[203,1978,1979],{"class":219}," client:\n",[203,1981,1982,1985,1987,1990],{"class":122,"line":496},[203,1983,1984],{"class":215},"        while",[203,1986,1939],{"class":219},[203,1988,1989],{"class":215},"\u003C",[203,1991,1992],{"class":219}," deadline:\n",[203,1994,1995,1998,2000,2002],{"class":122,"line":510},[203,1996,1997],{"class":219},"            error_rate, p95_ms, live_sha ",[203,1999,375],{"class":215},[203,2001,1833],{"class":215},[203,2003,2004],{"class":219}," sample(client)\n",[203,2006,2007,2010,2013,2016,2019],{"class":122,"line":521},[203,2008,2009],{"class":215},"            if",[203,2011,2012],{"class":219}," live_sha ",[203,2014,2015],{"class":215},"!=",[203,2017,2018],{"class":299}," RELEASE_SHA",[203,2020,427],{"class":219},[203,2022,2023,2026],{"class":122,"line":527},[203,2024,2025],{"class":215},"                return",[203,2027,2028],{"class":309}," \"flapping\"\n",[203,2030,2031,2033,2036,2039,2042],{"class":122,"line":533},[203,2032,2009],{"class":215},[203,2034,2035],{"class":219}," error_rate ",[203,2037,2038],{"class":215},">",[203,2040,2041],{"class":299}," MAX_ERROR_RATE",[203,2043,427],{"class":219},[203,2045,2046,2048],{"class":122,"line":542},[203,2047,2025],{"class":215},[203,2049,2050],{"class":309}," \"errors\"\n",[203,2052,2053,2055,2058,2060,2063],{"class":122,"line":555},[203,2054,2009],{"class":215},[203,2056,2057],{"class":219}," p95_ms ",[203,2059,2038],{"class":215},[203,2061,2062],{"class":299}," P95_BUDGET_MS",[203,2064,427],{"class":219},[203,2066,2067,2069],{"class":122,"line":563},[203,2068,2025],{"class":215},[203,2070,2071],{"class":309}," \"latency\"\n",[203,2073,2074,2077,2079,2082],{"class":122,"line":568},[203,2075,2076],{"class":215},"            await",[203,2078,548],{"class":219},[203,2080,2081],{"class":299},"15",[203,2083,361],{"class":219},[203,2085,2086,2088],{"class":122,"line":573},[203,2087,629],{"class":215},[203,2089,2090],{"class":309}," \"healthy\"\n",[203,2092,2093],{"class":122,"line":597},[203,2094,249],{"emptyLinePlaceholder":248},[203,2096,2097],{"class":122,"line":620},[203,2098,249],{"emptyLinePlaceholder":248},[203,2100,2101,2103,2105,2108,2110,2113],{"class":122,"line":626},[203,2102,466],{"class":215},[203,2104,469],{"class":215},[203,2106,2107],{"class":423}," main",[203,2109,1348],{"class":219},[203,2111,2112],{"class":299},"int",[203,2114,427],{"class":219},[203,2116,2117,2120,2122],{"class":122,"line":656},[203,2118,2119],{"class":215},"    match",[203,2121,1833],{"class":215},[203,2123,2124],{"class":219}," verdict():\n",[203,2126,2127,2130,2133],{"class":122,"line":661},[203,2128,2129],{"class":215},"        case",[203,2131,2132],{"class":309}," \"healthy\"",[203,2134,427],{"class":219},[203,2136,2137,2140,2142,2145,2148,2151,2154],{"class":122,"line":666},[203,2138,2139],{"class":299},"            print",[203,2141,579],{"class":219},[203,2143,2144],{"class":215},"f",[203,2146,2147],{"class":309},"\"release ",[203,2149,2150],{"class":299},"{RELEASE_SHA}",[203,2152,2153],{"class":309}," promoted\"",[203,2155,361],{"class":219},[203,2157,2158,2161],{"class":122,"line":686},[203,2159,2160],{"class":215},"            return",[203,2162,2163],{"class":299}," 0\n",[203,2165,2166,2168],{"class":122,"line":707},[203,2167,2129],{"class":215},[203,2169,2170],{"class":219}," reason:\n",[203,2172,2173,2175,2177,2179,2182,2184,2186,2188,2191,2194,2197,2199,2202,2204],{"class":122,"line":713},[203,2174,2139],{"class":299},[203,2176,579],{"class":219},[203,2178,2144],{"class":215},[203,2180,2181],{"class":309},"\"rolling back ",[203,2183,2150],{"class":299},[203,2185,638],{"class":309},[203,2187,1849],{"class":299},[203,2189,2190],{"class":219},"reason",[203,2192,2193],{"class":299},"}",[203,2195,2196],{"class":309},"\"",[203,2198,333],{"class":219},[203,2200,2201],{"class":385},"file",[203,2203,375],{"class":215},[203,2205,2206],{"class":219},"sys.stderr)\n",[203,2208,2209,2211],{"class":122,"line":725},[203,2210,2160],{"class":215},[203,2212,2213],{"class":299}," 1\n",[203,2215,2216],{"class":122,"line":736},[203,2217,249],{"emptyLinePlaceholder":248},[203,2219,2220],{"class":122,"line":761},[203,2221,249],{"emptyLinePlaceholder":248},[203,2223,2224,2227,2230,2233,2236],{"class":122,"line":785},[203,2225,2226],{"class":215},"if",[203,2228,2229],{"class":299}," __name__",[203,2231,2232],{"class":215}," ==",[203,2234,2235],{"class":309}," \"__main__\"",[203,2237,427],{"class":219},[203,2239,2240,2243,2246],{"class":122,"line":790},[203,2241,2242],{"class":215},"    raise",[203,2244,2245],{"class":299}," SystemExit",[203,2247,2248],{"class":219},"(asyncio.run(main()))\n",[14,2250,2251,2252,333,2255,2258,2259,2263],{},"Wire a non-zero exit to your platform's rollback command — ",[184,2253,2254],{},"render deploys rollback",[184,2256,2257],{},"flyctl releases rollback",", or simply redeploying the previous image tag. Immutable image tags are what make this instant: rolling back should re-point traffic at a container that already exists, never rebuild from source. That is one of the strongest arguments for ",[18,2260,2262],{"href":2261},"\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002F","containerizing your Python API with Docker"," even on a platform that would happily build from a buildpack.",[41,2265,50,2270,50,2273,50,2276,50,2278,50,2280,50,2284,50,2287,50,2290,50,2294,50,2297,50,2300,50,2304,50,2306,50,2309,50,2311,50,2315,50,2318,50,2321,50,2324,50,2326,50,2328,50,2330,50,2334,50,2337,50,2340,50,2343,50,2345,50,2348,50,2350,50,2354,50,2357,50,2362,50,2365],{"viewBox":2266,"role":44,"ariaLabelledBy":2267,"xmlns":48,"style":49},"0 0 720 360",[2268,2269],"zdd-rollback-t","zdd-rollback-d",[52,2271,2272],{"id":2268},"Automatic rollback decision tree",[56,2274,2275],{"id":2269},"During a ten minute watch window, an elevated error rate, doubled p95 latency, or flapping readiness each trigger an automatic rollback to the previous release.",[60,2277],{"x":62,"y":62,"width":63,"height":138,"fill":65},[60,2279],{"x":865,"y":865,"width":905,"height":1568,"rx":142,"fill":89,"stroke":90,"style":91},[67,2281,2283],{"x":101,"y":2282,"fill":77,"style":96},"47","Deploy v2, watch 10 min",[122,2285],{"x1":101,"y1":2286,"x2":101,"y2":1587,"stroke":71,"style":91},"64",[60,2288],{"x":865,"y":1587,"width":905,"height":2289,"rx":142,"fill":89,"stroke":126,"style":91},"40",[67,2291,2293],{"x":101,"y":2292,"fill":77,"style":96},"115","5xx rate over 1%?",[122,2295],{"x1":1146,"y1":1080,"x2":2296,"y2":1080,"stroke":102,"style":91},"466",[1111,2298],{"points":2299,"fill":102},"470,110 460,105 460,115",[67,2301,2303],{"x":156,"y":2302,"fill":77,"style":96},"102","yes",[122,2305],{"x1":101,"y1":1120,"x2":101,"y2":85,"stroke":71,"style":91},[67,2307,2308],{"x":139,"y":934,"fill":71,"style":72},"no",[60,2310],{"x":865,"y":85,"width":905,"height":2289,"rx":142,"fill":89,"stroke":126,"style":91},[67,2312,2314],{"x":101,"y":2313,"fill":77,"style":96},"185","p95 over budget?",[122,2316],{"x1":1146,"y1":2317,"x2":2296,"y2":2317,"stroke":102,"style":91},"180",[1111,2319],{"points":2320,"fill":102},"470,180 460,175 460,185",[67,2322,2303],{"x":156,"y":2323,"fill":77,"style":96},"172",[122,2325],{"x1":101,"y1":905,"x2":101,"y2":850,"stroke":71,"style":91},[67,2327,2308],{"x":139,"y":1146,"fill":71,"style":72},[60,2329],{"x":865,"y":850,"width":905,"height":2289,"rx":142,"fill":89,"stroke":126,"style":91},[67,2331,2333],{"x":101,"y":2332,"fill":77,"style":96},"255","readiness flapping?",[122,2335],{"x1":1146,"y1":2336,"x2":2296,"y2":2336,"stroke":102,"style":91},"250",[1111,2338],{"points":2339,"fill":102},"470,250 460,245 460,255",[67,2341,2303],{"x":156,"y":2342,"fill":77,"style":96},"242",[122,2344],{"x1":101,"y1":64,"x2":101,"y2":148,"stroke":71,"style":91},[67,2346,2308],{"x":139,"y":2347,"fill":71,"style":72},"290",[60,2349],{"x":865,"y":148,"width":905,"height":1568,"rx":142,"fill":89,"stroke":112,"style":91},[67,2351,2353],{"x":101,"y":2352,"fill":77,"style":96},"327","All clear, promote",[60,2355],{"x":2356,"y":1587,"width":850,"height":2317,"rx":1570,"fill":89,"stroke":102,"style":91},"470",[67,2358,2361],{"x":2359,"y":85,"fill":77,"style":2360},"585","text-anchor:middle;font-size:14;font-family:var(--font-sans);","Roll back to v1",[67,2363,2364],{"x":2359,"y":1613,"fill":71,"style":96},"re-point at the previous",[67,2366,2368],{"x":2359,"y":2367,"fill":71,"style":96},"208","image tag, under 60 s",[27,2370],{},[30,2372,2374],{"id":2373},"when-this-is-worth-building-and-when-it-is-not","When this is worth building, and when it is not",[14,2376,2377],{},"Build the full stack — readiness gate, drain, expand-contract, automatic rollback — the day you take money for the API. The cost is one afternoon and roughly zero ongoing compute; the alternative is refunding customers for failures you caused on purpose. If you are on a paid instance already, running two instances during a rolling deploy costs nothing extra on Render or Railway because the overlap lasts under a minute.",[14,2379,2380,2381,2384],{},"Skip the automatic rollback watcher, but nothing else, when you are pre-revenue on a single instance. A rolling deploy cannot overlap at all unless the platform briefly runs two instances, so check that setting first: on Fly it is ",[184,2382,2383],{},"min_machines_running"," plus a rolling strategy, on Render it is any paid plan. Skip the whole thing only for an internal tool where thirty seconds of downtime costs nothing.",[14,2386,2387],{},"The one case that demands more is a high-traffic API where a bad release costs thousands per minute. There, graduate to canary: route 5% of traffic to the new release, compare error rates against the control, and promote or discard. The mechanisms above are its prerequisites — canary routing without a readiness gate and reversible migrations is just a faster way to break production.",[2389,2390,2392],"h3",{"id":2391},"the-transition-path-from-where-you-are-now","The transition path from where you are now",[14,2394,2395,2396,2399,2400,2402,2403,2405,2406,2408,2409,2412,2413,1047],{},"If you are deploying by pushing to ",[184,2397,2398],{},"main"," and hoping, four changes get you to zero-downtime in order. First, add ",[184,2401,826],{}," and ",[184,2404,186],{}," and point the platform's health check at readiness. Second, add ",[184,2407,1038],{}," plus ",[184,2410,2411],{},"--timeout-graceful-shutdown"," to your start command and raise the platform grace period above it. Third, move migrations out of the app start command into a separate release step, and split the next breaking schema change into expand and contract. Fourth, add the watch script to the end of your deploy job. Each step ships independently. Verify the drain step by running a load generator against staging while you deploy and confirming zero non-2xx responses — an assertion worth keeping once you have ",[18,2414,2416],{"href":2415},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002F","tested the endpoints with pytest",[27,2418],{},[30,2420,2422],{"id":2421},"builder-verdict","Builder verdict",[14,2424,2425],{},"Rolling deploys with a readiness gate and a graceful drain win for every commercial Python API under roughly a thousand requests per second, and it is not close. You get the availability of blue-green at a fraction of the cost, because the overlap lasts seconds instead of requiring a duplicate environment you pay for continuously. Start with the four mechanisms here, keep migrations expand-contract forever, and buy more sophistication only when a minute of degraded service costs more than the engineering time to prevent it. The highest-leverage code on this page is the readiness flag flip in your shutdown handler — three lines that eliminate most deploy-time 502s on their own.",[27,2427],{},[30,2429,2431],{"id":2430},"faq","FAQ",[14,2433,2434,2437],{},[1179,2435,2436],{},"Does running two instances during a rolling deploy double my hosting bill?","\nNo. The overlap lasts thirty to ninety seconds per release, so even at ten deploys a day you are paying for a few extra instance-minutes — cents per month on usage-billed platforms, and nothing at all on flat per-service plans where the overlap is included. Compare that with the refund and churn cost of one visibly broken release.",[14,2439,2440,2443],{},[1179,2441,2442],{},"Should I run database migrations as part of the app's start command?","\nNever. Every instance would race to run them simultaneously, and a failed migration would crash-loop your whole service instead of failing one job. Run migrations as a separate release step that completes before the new instances boot, and keep them backwards compatible so the old code surviving the overlap is unaffected.",[14,2445,2446,2449],{},[1179,2447,2448],{},"How long should the drain window be for a metered API?","\nLong enough for your slowest endpoint plus a probe cycle, which for most APIs means twenty to thirty seconds. Check your p99 latency and any billing writes that must complete — a usage event that never lands is revenue you cannot invoice, so the drain has to outlast your longest write path, not your median request.",[14,2451,2452,2455],{},[1179,2453,2454],{},"What is the real risk of skipping the contract step and leaving old columns?","\nLow technical risk, mounting cost. Dead columns bloat rows, slow sequential scans, and confuse the next person reading the model, and unused indexes still get written on every insert. Schedule contract migrations a few days after the expand release so rollback stays possible, then actually run them rather than leaving debt to accumulate.",[14,2457,2458,2461],{},[1179,2459,2460],{},"Can I do zero-downtime deploys while also rotating secrets?","\nYes, and the pattern is identical: expand before you contract. Add the new credential, deploy code that accepts both old and new, then remove the old one in a later release. The same overlap logic applied to customer credentials is covered in detail in the guide on rotating API keys without downtime.",[30,2463,2465],{"id":2464},"related","Related",[14,2467,2468],{},[1179,2469,2470],{},"Same track:",[2472,2473,2474,2480,2487],"ul",{},[2475,2476,2477,2479],"li",{},[18,2478,21],{"href":20}," — the parent guide covering platform choice and deploy configuration end to end.",[2475,2481,2482,2486],{},[18,2483,2485],{"href":2484},"\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 platform's rolling deploy and health check settings differ.",[2475,2488,2489,2493],{},[18,2490,2492],{"href":2491},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fdeploying-fastapi-to-cloudflare-workers-with-python\u002F","Deploying FastAPI to Cloudflare Workers with Python"," — what changes when there is no long-lived process to drain.",[14,2495,2496],{},[1179,2497,2498],{},"Other tracks:",[2472,2500,2501,2508],{},[2475,2502,2503,2507],{},[18,2504,2506],{"href":2505},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002Frotating-api-keys-without-downtime\u002F","Rotating API Keys Without Downtime"," — the same expand-contract discipline applied to customer credentials.",[2475,2509,2510,2513],{},[18,2511,2512],{"href":190},"Async Database Access with SQLAlchemy"," — pool configuration that decides how fast a new instance becomes ready.",[2515,2516,2517],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .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 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);}",{"title":199,"searchDepth":212,"depth":212,"links":2519},[2520,2521,2522,2523,2524,2525,2528,2529,2530],{"id":32,"depth":212,"text":33},{"id":175,"depth":212,"text":176},{"id":952,"depth":212,"text":953},{"id":1166,"depth":212,"text":1167},{"id":1639,"depth":212,"text":1640},{"id":2373,"depth":212,"text":2374,"children":2526},[2527],{"id":2391,"depth":223,"text":2392},{"id":2421,"depth":212,"text":2422},{"id":2430,"depth":212,"text":2431},{"id":2464,"depth":212,"text":2465},"Ship Python API releases without dropped requests: readiness gates, graceful drain on SIGTERM, expand-contract migrations, and automatic rollback triggers.","md",{"pageTitle":2534,"type":2535,"datePublished":2536,"dateModified":2536},"Zero-Downtime Deploys for Python APIs: A Builder Playbook","article","2026-07-23","\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis",{"title":5,"description":2531},"building-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002Findex","0Byr-0s-i-KdFEa6o0bZfaHZS6gnvCcQeISMX7bUkLM",{"@context":2542,"@type":2543,"mainEntity":2544},"https:\u002F\u002Fschema.org","FAQPage",[2545,2550,2553,2556,2559],{"@type":2546,"name":2436,"acceptedAnswer":2547},"Question",{"@type":2548,"text":2549},"Answer","No. The overlap lasts thirty to ninety seconds per release, so even at ten deploys a day you are paying for a few extra instance-minutes — cents per month on usage-billed platforms, and nothing at all on flat per-service plans where the overlap is included. Compare that with the refund and churn cost of one visibly broken release.",{"@type":2546,"name":2442,"acceptedAnswer":2551},{"@type":2548,"text":2552},"Never. Every instance would race to run them simultaneously, and a failed migration would crash-loop your whole service instead of failing one job. Run migrations as a separate release step that completes before the new instances boot, and keep them backwards compatible so the old code surviving the overlap is unaffected.",{"@type":2546,"name":2448,"acceptedAnswer":2554},{"@type":2548,"text":2555},"Long enough for your slowest endpoint plus a probe cycle, which for most APIs means twenty to thirty seconds. Check your p99 latency and any billing writes that must complete — a usage event that never lands is revenue you cannot invoice, so the drain has to outlast your longest write path, not your median request.",{"@type":2546,"name":2454,"acceptedAnswer":2557},{"@type":2548,"text":2558},"Low technical risk, mounting cost. Dead columns bloat rows, slow sequential scans, and confuse the next person reading the model, and unused indexes still get written on every insert. Schedule contract migrations a few days after the expand release so rollback stays possible, then actually run them rather than leaving debt to accumulate.",{"@type":2546,"name":2460,"acceptedAnswer":2560},{"@type":2548,"text":2561},"Yes, and the pattern is identical: expand before you contract. Add the new credential, deploy code that accepts both old and new, then remove the old one in a later release. The same overlap logic applied to customer credentials is covered in detail in the guide on rotating API keys without downtime.",1784887028387]