[{"data":1,"prerenderedAt":2242},["ShallowReactive",2],{"page-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fdeploying-fastapi-to-cloudflare-workers-with-python\u002F":3,"faq-schema-\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fdeploying-fastapi-to-cloudflare-workers-with-python\u002F":2221},{"id":4,"title":5,"body":6,"description":2212,"extension":2213,"meta":2214,"navigation":298,"path":2217,"seo":2218,"stem":2219,"__hash__":2220},"content\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fdeploying-fastapi-to-cloudflare-workers-with-python\u002Findex.md","Deploying FastAPI to Cloudflare Workers with Python",{"type":7,"value":8,"toc":2200},"minimark",[9,13,28,31,36,52,67,220,222,226,242,1013,1038,1041,1097,1108,1110,1114,1117,1205,1220,1313,1315,1319,1322,1330,1420,1422,1426,1434,1880,1891,1893,1897,1905,1918,2032,2034,2038,2041,2094,2097,2099,2103,2111,2113,2117,2124,2130,2136,2142,2148,2152,2157,2177,2182,2196],[10,11,5],"h1",{"id":12},"deploying-fastapi-to-cloudflare-workers-with-python",[14,15,16,17,21,22,27],"p",{},"Cloudflare will run your FastAPI app in 330-plus cities for five dollars a month, and that number makes every builder pause before renewing a container plan. The catch is that \"Python on Workers\" does not mean CPython on a Linux box — it means Pyodide, a WebAssembly build of Python, inside a V8 isolate with no filesystem, no threads, no TCP sockets, and no ",[18,19,20],"code",{},"pip install"," at runtime. Part of the ",[23,24,26],"a",{"href":25},"\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: does your commercial API belong on Python Workers, or does it belong on a container host? The answer turns on your dependency tree and your data layer, not on latency benchmarks.",[29,30],"hr",{},[32,33,35],"h2",{"id":34},"what-actually-runs-on-a-python-worker","What actually runs on a Python Worker",[14,37,38,39,42,43,46,47,51],{},"A Workers deploy does not spawn a process. Cloudflare loads your module into a V8 isolate that already has a Pyodide interpreter compiled to WebAssembly, hands the request to your handler, and tears the isolate down when it goes idle. There is no ",[18,40,41],{},"uvicorn",", no worker pool, no ",[18,44,45],{},"WEB_CONCURRENCY"," to tune — a decision that dominates throughput on a normal box, as ",[23,48,50],{"href":49},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002Fuvicorn-vs-gunicorn-worker-configuration\u002F","Uvicorn vs Gunicorn worker configuration"," explains, and that simply does not exist here. Concurrency is the platform's problem: one isolate handles one request at a time, and Cloudflare spins up as many isolates as it needs across the edge network.",[14,53,54,55,58,59,62,63,66],{},"That model gives you three things a container never will: zero idle cost, automatic global placement, and a request path with no load balancer hop. It takes away everything that assumes an operating system. Your code cannot open a socket, fork a process, write to ",[18,56,57],{},"\u002Ftmp"," and expect the file to survive, or import a wheel that was not compiled for the ",[18,60,61],{},"wasm32-emscripten"," target. Every piece of state lives behind a binding — a KV namespace, a D1 database, an R2 bucket, a queue, or a Durable Object — injected as an object on ",[18,64,65],{},"env"," rather than reached over a connection string.",[68,69,77,78,77,82,77,86,77,93,77,103,77,111,77,117,77,121,77,125,77,128,77,132,77,136,77,139,77,144,77,148,77,151,77,155,77,158,77,161,77,165,77,168,77,171,77,176,77,182,77,188,77,192,77,195,77,199,77,202,77,205,77,209,77,212,77,216],"svg",{"viewBox":70,"role":71,"ariaLabelledBy":72,"xmlns":75,"style":76},"0 0 720 310","img",[73,74],"cfpy-arch-t","cfpy-arch-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[79,80,81],"title",{"id":73},"Request path through a Cloudflare Python Worker",[83,84,85],"desc",{"id":74},"An HTTP request enters a Workers isolate, runs through the Pyodide WebAssembly interpreter and an ASGI adapter into FastAPI, which reaches state only through KV, D1, R2 and queue bindings.",[87,88],"rect",{"x":89,"y":89,"width":90,"height":91,"fill":92},"0","720","310","var(--c-surface)",[87,94],{"x":95,"y":96,"width":97,"height":98,"rx":99,"fill":100,"stroke":101,"style":102},"16","46","150","76","10","var(--c-surface-alt)","var(--c-border)","stroke-width:2;",[104,105,110],"text",{"x":106,"y":107,"fill":108,"style":109},"91","80","var(--c-text)","text-anchor:middle;font-size:13;font-family:var(--font-sans);","HTTPS request",[104,112,116],{"x":106,"y":113,"fill":114,"style":115},"102","var(--c-text-muted)","text-anchor:middle;font-size:11;font-family:var(--font-sans);","nearest edge city",[87,118],{"x":119,"y":96,"width":97,"height":98,"rx":99,"fill":92,"stroke":120,"style":102},"190","var(--c-blue)",[104,122,124],{"x":123,"y":107,"fill":108,"style":109},"265","V8 isolate",[104,126,127],{"x":123,"y":113,"fill":114,"style":115},"no OS, no process",[87,129],{"x":130,"y":96,"width":97,"height":98,"rx":99,"fill":92,"stroke":131,"style":102},"364","var(--c-teal)",[104,133,135],{"x":134,"y":107,"fill":108,"style":109},"439","Pyodide + ASGI",[104,137,138],{"x":134,"y":113,"fill":114,"style":115},"Python on Wasm",[87,140],{"x":141,"y":96,"width":142,"height":98,"rx":99,"fill":92,"stroke":143,"style":102},"538","166","var(--c-coral)",[104,145,147],{"x":146,"y":107,"fill":108,"style":109},"621","FastAPI routes",[104,149,150],{"x":146,"y":113,"fill":114,"style":115},"your app object",[152,153],"path",{"d":154,"stroke":114,"style":102},"M166 84 L188 84",[152,156],{"d":157,"stroke":114,"style":102},"M340 84 L362 84",[152,159],{"d":160,"stroke":114,"style":102},"M514 84 L536 84",[152,162],{"d":163,"stroke":143,"fill":164,"style":102},"M621 122 L621 150 L120 150 L120 186","none",[152,166],{"d":167,"stroke":143,"fill":164,"style":102},"M621 150 L340 150 L340 186",[152,169],{"d":170,"stroke":143,"fill":164,"style":102},"M621 150 L560 150 L560 186",[104,172,175],{"x":173,"y":174,"fill":114,"style":115},"360","172","state reached only through bindings on env",[87,177],{"x":178,"y":179,"width":180,"height":181,"rx":99,"fill":100,"stroke":101,"style":102},"30","186","180","60",[104,183,187],{"x":184,"y":185,"fill":108,"style":186},"120","212","text-anchor:middle;font-size:12;font-family:var(--font-sans);","KV namespace",[104,189,191],{"x":184,"y":190,"fill":114,"style":115},"232","cache, quotas, keys",[87,193],{"x":194,"y":179,"width":180,"height":181,"rx":99,"fill":100,"stroke":101,"style":102},"250",[104,196,198],{"x":197,"y":185,"fill":108,"style":186},"340","D1 database",[104,200,201],{"x":197,"y":190,"fill":114,"style":115},"SQLite, no pool",[87,203],{"x":204,"y":179,"width":180,"height":181,"rx":99,"fill":100,"stroke":101,"style":102},"470",[104,206,208],{"x":207,"y":185,"fill":108,"style":186},"560","R2 and Queues",[104,210,211],{"x":207,"y":190,"fill":114,"style":115},"blobs, async work",[104,213,215],{"x":173,"y":214,"fill":108,"style":186},"278","No sockets: asyncpg, redis-py and psycopg cannot connect",[104,217,219],{"x":173,"y":218,"fill":114,"style":115},"298","outbound traffic goes through fetch, not TCP",[29,221],{},[32,223,225],{"id":224},"the-asgi-bridge-in-code","The ASGI bridge, in code",[14,227,228,229,232,233,236,237,241],{},"FastAPI is an ASGI application, and Cloudflare ships an adapter that translates a Workers ",[18,230,231],{},"Request"," into an ASGI scope and back. Your entry module exports ",[18,234,235],{},"on_fetch",", and the adapter drives your app. If you already know ",[23,238,240],{"href":239},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002F","how to set up FastAPI",", nothing about routing, dependencies, or Pydantic models changes — only the process boundary does.",[243,244,249],"pre",{"className":245,"code":246,"language":247,"meta":248,"style":248},"language-python shiki shiki-themes github-light github-dark","# src\u002Fentry.py — runs on Cloudflare Python Workers\nimport os\nimport json\nfrom fastapi import FastAPI, Depends, HTTPException, Request\n\nimport asgi  # provided by the Workers Python runtime\n\napp = FastAPI(title=os.getenv(\"API_TITLE\", \"edge-api\"))\n\n# The Workers env object is not os.environ; stash it per request so\n# dependencies can read bindings and secrets from one place.\n_runtime: dict[str, object] = {}\n\n\ndef settings(request: Request) -> dict[str, object]:\n    env = _runtime.get(\"env\")\n    return {\n        \"env\": env,\n        \"free_quota\": int(os.getenv(\"FREE_TIER_DAILY_QUOTA\", \"1000\")),\n        \"api_key\": getattr(env, \"API_SIGNING_KEY\", None),\n    }\n\n\n@app.get(\"\u002Fhealthz\")\nasync def healthz() -> dict[str, str]:\n    return {\"status\": \"ok\", \"runtime\": \"workers-python\"}\n\n\n@app.get(\"\u002Fv1\u002Fquote\u002F{symbol}\")\nasync def quote(symbol: str, cfg: dict = Depends(settings)) -> dict:\n    env = cfg[\"env\"]\n    cached = await env.QUOTES.get(f\"quote:{symbol}\")\n    if cached is not None:\n        return json.loads(cached)\n\n    upstream = os.getenv(\"UPSTREAM_QUOTE_URL\")\n    if not upstream:\n        raise HTTPException(status_code=503, detail=\"upstream not configured\")\n\n    # fetch() is the only outbound transport available in an isolate.\n    from js import fetch  # noqa: PLC0415\n    response = await fetch(f\"{upstream}?symbol={symbol}\")\n    if response.status != 200:\n        raise HTTPException(status_code=502, detail=\"upstream failed\")\n\n    payload = json.loads(await response.text())\n    await env.QUOTES.put(f\"quote:{symbol}\", json.dumps(payload), expirationTtl=60)\n    return payload\n\n\nasync def on_fetch(request, env):\n    _runtime[\"env\"] = env\n    return await asgi.fetch(app, request, env)\n","python","",[18,250,251,260,271,279,293,300,311,316,349,354,360,366,389,394,399,421,438,447,456,482,507,513,518,523,537,560,589,594,599,617,649,664,703,723,732,737,753,763,792,797,803,820,857,873,898,903,920,957,965,970,975,988,1003],{"__ignoreMap":248},[252,253,256],"span",{"class":254,"line":255},"line",1,[252,257,259],{"class":258},"sJ8bj","# src\u002Fentry.py — runs on Cloudflare Python Workers\n",[252,261,263,267],{"class":254,"line":262},2,[252,264,266],{"class":265},"szBVR","import",[252,268,270],{"class":269},"sVt8B"," os\n",[252,272,274,276],{"class":254,"line":273},3,[252,275,266],{"class":265},[252,277,278],{"class":269}," json\n",[252,280,282,285,288,290],{"class":254,"line":281},4,[252,283,284],{"class":265},"from",[252,286,287],{"class":269}," fastapi ",[252,289,266],{"class":265},[252,291,292],{"class":269}," FastAPI, Depends, HTTPException, Request\n",[252,294,296],{"class":254,"line":295},5,[252,297,299],{"emptyLinePlaceholder":298},true,"\n",[252,301,303,305,308],{"class":254,"line":302},6,[252,304,266],{"class":265},[252,306,307],{"class":269}," asgi  ",[252,309,310],{"class":258},"# provided by the Workers Python runtime\n",[252,312,314],{"class":254,"line":313},7,[252,315,299],{"emptyLinePlaceholder":298},[252,317,319,322,325,328,331,333,336,340,343,346],{"class":254,"line":318},8,[252,320,321],{"class":269},"app ",[252,323,324],{"class":265},"=",[252,326,327],{"class":269}," FastAPI(",[252,329,79],{"class":330},"s4XuR",[252,332,324],{"class":265},[252,334,335],{"class":269},"os.getenv(",[252,337,339],{"class":338},"sZZnC","\"API_TITLE\"",[252,341,342],{"class":269},", ",[252,344,345],{"class":338},"\"edge-api\"",[252,347,348],{"class":269},"))\n",[252,350,352],{"class":254,"line":351},9,[252,353,299],{"emptyLinePlaceholder":298},[252,355,357],{"class":254,"line":356},10,[252,358,359],{"class":258},"# The Workers env object is not os.environ; stash it per request so\n",[252,361,363],{"class":254,"line":362},11,[252,364,365],{"class":258},"# dependencies can read bindings and secrets from one place.\n",[252,367,369,372,376,378,381,384,386],{"class":254,"line":368},12,[252,370,371],{"class":269},"_runtime: dict[",[252,373,375],{"class":374},"sj4cs","str",[252,377,342],{"class":269},[252,379,380],{"class":374},"object",[252,382,383],{"class":269},"] ",[252,385,324],{"class":265},[252,387,388],{"class":269}," {}\n",[252,390,392],{"class":254,"line":391},13,[252,393,299],{"emptyLinePlaceholder":298},[252,395,397],{"class":254,"line":396},14,[252,398,299],{"emptyLinePlaceholder":298},[252,400,402,405,409,412,414,416,418],{"class":254,"line":401},15,[252,403,404],{"class":265},"def",[252,406,408],{"class":407},"sScJk"," settings",[252,410,411],{"class":269},"(request: Request) -> dict[",[252,413,375],{"class":374},[252,415,342],{"class":269},[252,417,380],{"class":374},[252,419,420],{"class":269},"]:\n",[252,422,424,427,429,432,435],{"class":254,"line":423},16,[252,425,426],{"class":269},"    env ",[252,428,324],{"class":265},[252,430,431],{"class":269}," _runtime.get(",[252,433,434],{"class":338},"\"env\"",[252,436,437],{"class":269},")\n",[252,439,441,444],{"class":254,"line":440},17,[252,442,443],{"class":265},"    return",[252,445,446],{"class":269}," {\n",[252,448,450,453],{"class":254,"line":449},18,[252,451,452],{"class":338},"        \"env\"",[252,454,455],{"class":269},": env,\n",[252,457,459,462,465,468,471,474,476,479],{"class":254,"line":458},19,[252,460,461],{"class":338},"        \"free_quota\"",[252,463,464],{"class":269},": ",[252,466,467],{"class":374},"int",[252,469,470],{"class":269},"(os.getenv(",[252,472,473],{"class":338},"\"FREE_TIER_DAILY_QUOTA\"",[252,475,342],{"class":269},[252,477,478],{"class":338},"\"1000\"",[252,480,481],{"class":269},")),\n",[252,483,485,488,490,493,496,499,501,504],{"class":254,"line":484},20,[252,486,487],{"class":338},"        \"api_key\"",[252,489,464],{"class":269},[252,491,492],{"class":374},"getattr",[252,494,495],{"class":269},"(env, ",[252,497,498],{"class":338},"\"API_SIGNING_KEY\"",[252,500,342],{"class":269},[252,502,503],{"class":374},"None",[252,505,506],{"class":269},"),\n",[252,508,510],{"class":254,"line":509},21,[252,511,512],{"class":269},"    }\n",[252,514,516],{"class":254,"line":515},22,[252,517,299],{"emptyLinePlaceholder":298},[252,519,521],{"class":254,"line":520},23,[252,522,299],{"emptyLinePlaceholder":298},[252,524,526,529,532,535],{"class":254,"line":525},24,[252,527,528],{"class":407},"@app.get",[252,530,531],{"class":269},"(",[252,533,534],{"class":338},"\"\u002Fhealthz\"",[252,536,437],{"class":269},[252,538,540,543,546,549,552,554,556,558],{"class":254,"line":539},25,[252,541,542],{"class":265},"async",[252,544,545],{"class":265}," def",[252,547,548],{"class":407}," healthz",[252,550,551],{"class":269},"() -> dict[",[252,553,375],{"class":374},[252,555,342],{"class":269},[252,557,375],{"class":374},[252,559,420],{"class":269},[252,561,563,565,568,571,573,576,578,581,583,586],{"class":254,"line":562},26,[252,564,443],{"class":265},[252,566,567],{"class":269}," {",[252,569,570],{"class":338},"\"status\"",[252,572,464],{"class":269},[252,574,575],{"class":338},"\"ok\"",[252,577,342],{"class":269},[252,579,580],{"class":338},"\"runtime\"",[252,582,464],{"class":269},[252,584,585],{"class":338},"\"workers-python\"",[252,587,588],{"class":269},"}\n",[252,590,592],{"class":254,"line":591},27,[252,593,299],{"emptyLinePlaceholder":298},[252,595,597],{"class":254,"line":596},28,[252,598,299],{"emptyLinePlaceholder":298},[252,600,602,604,606,609,612,615],{"class":254,"line":601},29,[252,603,528],{"class":407},[252,605,531],{"class":269},[252,607,608],{"class":338},"\"\u002Fv1\u002Fquote\u002F",[252,610,611],{"class":374},"{symbol}",[252,613,614],{"class":338},"\"",[252,616,437],{"class":269},[252,618,620,622,624,627,630,632,635,638,641,644,646],{"class":254,"line":619},30,[252,621,542],{"class":265},[252,623,545],{"class":265},[252,625,626],{"class":407}," quote",[252,628,629],{"class":269},"(symbol: ",[252,631,375],{"class":374},[252,633,634],{"class":269},", cfg: ",[252,636,637],{"class":374},"dict",[252,639,640],{"class":265}," =",[252,642,643],{"class":269}," Depends(settings)) -> ",[252,645,637],{"class":374},[252,647,648],{"class":269},":\n",[252,650,652,654,656,659,661],{"class":254,"line":651},31,[252,653,426],{"class":269},[252,655,324],{"class":265},[252,657,658],{"class":269}," cfg[",[252,660,434],{"class":338},[252,662,663],{"class":269},"]\n",[252,665,667,670,672,675,678,681,684,687,690,693,696,699,701],{"class":254,"line":666},32,[252,668,669],{"class":269},"    cached ",[252,671,324],{"class":265},[252,673,674],{"class":265}," await",[252,676,677],{"class":269}," env.",[252,679,680],{"class":374},"QUOTES",[252,682,683],{"class":269},".get(",[252,685,686],{"class":265},"f",[252,688,689],{"class":338},"\"quote:",[252,691,692],{"class":374},"{",[252,694,695],{"class":269},"symbol",[252,697,698],{"class":374},"}",[252,700,614],{"class":338},[252,702,437],{"class":269},[252,704,706,709,712,715,718,721],{"class":254,"line":705},33,[252,707,708],{"class":265},"    if",[252,710,711],{"class":269}," cached ",[252,713,714],{"class":265},"is",[252,716,717],{"class":265}," not",[252,719,720],{"class":374}," None",[252,722,648],{"class":269},[252,724,726,729],{"class":254,"line":725},34,[252,727,728],{"class":265},"        return",[252,730,731],{"class":269}," json.loads(cached)\n",[252,733,735],{"class":254,"line":734},35,[252,736,299],{"emptyLinePlaceholder":298},[252,738,740,743,745,748,751],{"class":254,"line":739},36,[252,741,742],{"class":269},"    upstream ",[252,744,324],{"class":265},[252,746,747],{"class":269}," os.getenv(",[252,749,750],{"class":338},"\"UPSTREAM_QUOTE_URL\"",[252,752,437],{"class":269},[252,754,756,758,760],{"class":254,"line":755},37,[252,757,708],{"class":265},[252,759,717],{"class":265},[252,761,762],{"class":269}," upstream:\n",[252,764,766,769,772,775,777,780,782,785,787,790],{"class":254,"line":765},38,[252,767,768],{"class":265},"        raise",[252,770,771],{"class":269}," HTTPException(",[252,773,774],{"class":330},"status_code",[252,776,324],{"class":265},[252,778,779],{"class":374},"503",[252,781,342],{"class":269},[252,783,784],{"class":330},"detail",[252,786,324],{"class":265},[252,788,789],{"class":338},"\"upstream not configured\"",[252,791,437],{"class":269},[252,793,795],{"class":254,"line":794},39,[252,796,299],{"emptyLinePlaceholder":298},[252,798,800],{"class":254,"line":799},40,[252,801,802],{"class":258},"    # fetch() is the only outbound transport available in an isolate.\n",[252,804,806,809,812,814,817],{"class":254,"line":805},41,[252,807,808],{"class":265},"    from",[252,810,811],{"class":269}," js ",[252,813,266],{"class":265},[252,815,816],{"class":269}," fetch  ",[252,818,819],{"class":258},"# noqa: PLC0415\n",[252,821,823,826,828,830,833,835,837,839,842,844,847,849,851,853,855],{"class":254,"line":822},42,[252,824,825],{"class":269},"    response ",[252,827,324],{"class":265},[252,829,674],{"class":265},[252,831,832],{"class":269}," fetch(",[252,834,686],{"class":265},[252,836,614],{"class":338},[252,838,692],{"class":374},[252,840,841],{"class":269},"upstream",[252,843,698],{"class":374},[252,845,846],{"class":338},"?symbol=",[252,848,692],{"class":374},[252,850,695],{"class":269},[252,852,698],{"class":374},[252,854,614],{"class":338},[252,856,437],{"class":269},[252,858,860,862,865,868,871],{"class":254,"line":859},43,[252,861,708],{"class":265},[252,863,864],{"class":269}," response.status ",[252,866,867],{"class":265},"!=",[252,869,870],{"class":374}," 200",[252,872,648],{"class":269},[252,874,876,878,880,882,884,887,889,891,893,896],{"class":254,"line":875},44,[252,877,768],{"class":265},[252,879,771],{"class":269},[252,881,774],{"class":330},[252,883,324],{"class":265},[252,885,886],{"class":374},"502",[252,888,342],{"class":269},[252,890,784],{"class":330},[252,892,324],{"class":265},[252,894,895],{"class":338},"\"upstream failed\"",[252,897,437],{"class":269},[252,899,901],{"class":254,"line":900},45,[252,902,299],{"emptyLinePlaceholder":298},[252,904,906,909,911,914,917],{"class":254,"line":905},46,[252,907,908],{"class":269},"    payload ",[252,910,324],{"class":265},[252,912,913],{"class":269}," json.loads(",[252,915,916],{"class":265},"await",[252,918,919],{"class":269}," response.text())\n",[252,921,923,926,928,930,933,935,937,939,941,943,945,948,951,953,955],{"class":254,"line":922},47,[252,924,925],{"class":265},"    await",[252,927,677],{"class":269},[252,929,680],{"class":374},[252,931,932],{"class":269},".put(",[252,934,686],{"class":265},[252,936,689],{"class":338},[252,938,692],{"class":374},[252,940,695],{"class":269},[252,942,698],{"class":374},[252,944,614],{"class":338},[252,946,947],{"class":269},", json.dumps(payload), ",[252,949,950],{"class":330},"expirationTtl",[252,952,324],{"class":265},[252,954,181],{"class":374},[252,956,437],{"class":269},[252,958,960,962],{"class":254,"line":959},48,[252,961,443],{"class":265},[252,963,964],{"class":269}," payload\n",[252,966,968],{"class":254,"line":967},49,[252,969,299],{"emptyLinePlaceholder":298},[252,971,973],{"class":254,"line":972},50,[252,974,299],{"emptyLinePlaceholder":298},[252,976,978,980,982,985],{"class":254,"line":977},51,[252,979,542],{"class":265},[252,981,545],{"class":265},[252,983,984],{"class":407}," on_fetch",[252,986,987],{"class":269},"(request, env):\n",[252,989,991,994,996,998,1000],{"class":254,"line":990},52,[252,992,993],{"class":269},"    _runtime[",[252,995,434],{"class":338},[252,997,383],{"class":269},[252,999,324],{"class":265},[252,1001,1002],{"class":269}," env\n",[252,1004,1006,1008,1010],{"class":254,"line":1005},53,[252,1007,443],{"class":265},[252,1009,674],{"class":265},[252,1011,1012],{"class":269}," asgi.fetch(app, request, env)\n",[14,1014,1015,1016,1019,1020,1024,1025,1028,1029,1033,1034,1037],{},"Two details matter commercially. First, ",[18,1017,1018],{},"env.QUOTES"," is a KV binding, so your cache is global and free of connection management — the trade-off against a Redis instance is covered in ",[23,1021,1023],{"href":1022},"\u002Fscaling-and-operating-production-python-apis\u002Fcaching-python-api-responses-with-redis\u002Fredis-vs-in-memory-caching-for-fastapi\u002F","Redis vs in-memory caching for FastAPI",". Second, ",[18,1026,1027],{},"from js import fetch"," is your only outbound path. An async HTTP client you know well, like the one compared in ",[23,1030,1032],{"href":1031},"\u002Fgetting-started-with-python-apis-for-builders\u002Fmaking-http-requests-with-requests-library\u002Fhttpx-vs-requests-for-async\u002F","httpx vs requests for async",", only works here if it has been ported to route through ",[18,1035,1036],{},"fetch","; assume the raw socket transport is unavailable until you test it.",[14,1039,1040],{},"The deploy config is small and belongs in version control:",[243,1042,1046],{"className":1043,"code":1044,"language":1045,"meta":248,"style":248},"language-jsonc shiki shiki-themes github-light github-dark","\u002F\u002F wrangler.jsonc\n{\n  \"name\": \"edge-api\",\n  \"main\": \"src\u002Fentry.py\",\n  \"compatibility_date\": \"2026-01-15\",\n  \"compatibility_flags\": [\"python_workers\"],\n  \"kv_namespaces\": [{ \"binding\": \"QUOTES\", \"id\": \"$KV_NAMESPACE_ID\" }],\n  \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"$D1_DATABASE_ID\" }],\n  \"observability\": { \"enabled\": true }\n}\n","jsonc",[18,1047,1048,1053,1058,1063,1068,1073,1078,1083,1088,1093],{"__ignoreMap":248},[252,1049,1050],{"class":254,"line":255},[252,1051,1052],{},"\u002F\u002F wrangler.jsonc\n",[252,1054,1055],{"class":254,"line":262},[252,1056,1057],{},"{\n",[252,1059,1060],{"class":254,"line":273},[252,1061,1062],{},"  \"name\": \"edge-api\",\n",[252,1064,1065],{"class":254,"line":281},[252,1066,1067],{},"  \"main\": \"src\u002Fentry.py\",\n",[252,1069,1070],{"class":254,"line":295},[252,1071,1072],{},"  \"compatibility_date\": \"2026-01-15\",\n",[252,1074,1075],{"class":254,"line":302},[252,1076,1077],{},"  \"compatibility_flags\": [\"python_workers\"],\n",[252,1079,1080],{"class":254,"line":313},[252,1081,1082],{},"  \"kv_namespaces\": [{ \"binding\": \"QUOTES\", \"id\": \"$KV_NAMESPACE_ID\" }],\n",[252,1084,1085],{"class":254,"line":318},[252,1086,1087],{},"  \"d1_databases\": [{ \"binding\": \"DB\", \"database_id\": \"$D1_DATABASE_ID\" }],\n",[252,1089,1090],{"class":254,"line":351},[252,1091,1092],{},"  \"observability\": { \"enabled\": true }\n",[252,1094,1095],{"class":254,"line":356},[252,1096,588],{},[14,1098,1099,1100,1103,1104,1107],{},"Dependencies go in a ",[18,1101,1102],{},"requirements.txt"," next to it, and they are resolved at deploy time from the packages Cloudflare bundles with the runtime. There is no arbitrary wheel install and no ",[18,1105,1106],{},"uv sync"," on the edge.",[29,1109],{},[32,1111,1113],{"id":1112},"the-dependency-wall","The dependency wall",[14,1115,1116],{},"This is where most migrations die. Pyodide only runs pure-Python packages and C extensions that Cloudflare has compiled to WebAssembly and shipped inside the runtime. FastAPI, Starlette, Pydantic, Jinja2, and a large slice of the pure-Python ecosystem are fine. Anything that opens a socket, links against libpq, spawns a thread, or needs a native binary is not.",[1118,1119,1120,1136],"table",{},[1121,1122,1123],"thead",{},[1124,1125,1126,1130,1133],"tr",{},[1127,1128,1129],"th",{},"Dependency",[1127,1131,1132],{},"Status on Python Workers",[1127,1134,1135],{},"Replacement",[1137,1138,1139,1151,1162,1172,1183,1194],"tbody",{},[1124,1140,1141,1145,1148],{},[1142,1143,1144],"td",{},"FastAPI, Pydantic v2",[1142,1146,1147],{},"Supported",[1142,1149,1150],{},"none needed",[1124,1152,1153,1156,1159],{},[1142,1154,1155],{},"asyncpg, psycopg3",[1142,1157,1158],{},"No TCP sockets",[1142,1160,1161],{},"D1 or Hyperdrive",[1124,1163,1164,1167,1169],{},[1142,1165,1166],{},"redis-py",[1142,1168,1158],{},[1142,1170,1171],{},"KV binding",[1124,1173,1174,1177,1180],{},[1142,1175,1176],{},"SQLAlchemy async ORM",[1142,1178,1179],{},"Driver unavailable",[1142,1181,1182],{},"D1 SQL, raw",[1124,1184,1185,1188,1191],{},[1142,1186,1187],{},"Celery, APScheduler",[1142,1189,1190],{},"No threads or process",[1142,1192,1193],{},"Queues, Cron Triggers",[1124,1195,1196,1199,1202],{},[1142,1197,1198],{},"boto3, requests",[1142,1200,1201],{},"Blocking sockets",[1142,1203,1204],{},"R2 binding, fetch",[14,1206,1207,1208,1210,1211,1214,1215,1219],{},"The practical test takes ten minutes: create a throwaway Worker, put your real ",[18,1209,1102],{}," in it, and run ",[18,1212,1213],{},"npx wrangler deploy --dry-run",". If the import graph resolves, you have a candidate. If your data layer is built on ",[23,1216,1218],{"href":1217},"\u002Fscaling-and-operating-production-python-apis\u002Fasync-database-access-with-sqlalchemy\u002Fasyncpg-vs-psycopg3-for-fastapi\u002F","async SQLAlchemy over asyncpg",", you are rewriting queries against D1's SQL API, not porting an app.",[68,1221,77,1226,77,1229,77,1232,77,1235,77,1240,77,1245,77,1251,77,1257,77,1261,77,1265,77,1269,77,1272,77,1276,77,1280,77,1283,77,1287,77,1291,77,1295,77,1298,77,1301,77,1304,77,1307,77,1310],{"viewBox":1222,"role":71,"ariaLabelledBy":1223,"xmlns":75,"style":76},"0 0 720 300",[1224,1225],"cfpy-deps-t","cfpy-deps-d",[79,1227,1228],{"id":1224},"Which Python dependencies survive the move to Workers",[83,1230,1231],{"id":1225},"Two columns dividing common Python API dependencies into those Pyodide runs today and those that require a container host or a binding-based rewrite.",[87,1233],{"x":89,"y":89,"width":90,"height":1234,"fill":92},"300",[87,1236],{"x":95,"y":95,"width":1237,"height":1238,"rx":1239,"fill":92,"stroke":131,"style":102},"336","268","12",[104,1241,1244],{"x":1242,"y":96,"fill":108,"style":1243},"184","text-anchor:middle;font-size:14;font-family:var(--font-sans);","Runs inside Pyodide",[254,1246],{"x1":1247,"y1":1248,"x2":1249,"y2":1248,"stroke":101,"style":1250},"36","58","332","stroke-width:1;",[104,1252,1256],{"x":1253,"y":1254,"fill":108,"style":1255},"40","86","font-size:12;font-family:var(--font-sans);","FastAPI and Starlette routing",[104,1258,1260],{"x":1253,"y":1259,"fill":108,"style":1255},"112","Pydantic v2 validation",[104,1262,1264],{"x":1253,"y":1263,"fill":108,"style":1255},"138","json, hmac, hashlib, base64",[104,1266,1268],{"x":1253,"y":1267,"fill":108,"style":1255},"164","PyJWT signing and verify",[104,1270,1271],{"x":1253,"y":119,"fill":108,"style":1255},"Jinja2 templates",[104,1273,1275],{"x":1253,"y":1274,"fill":108,"style":1255},"216","numpy via the Wasm build",[104,1277,1279],{"x":1253,"y":1278,"fill":131,"style":1255},"256","Verdict: deploy as-is",[87,1281],{"x":1282,"y":95,"width":1237,"height":1238,"rx":1239,"fill":92,"stroke":143,"style":102},"368",[104,1284,1286],{"x":1285,"y":96,"fill":108,"style":1243},"536","Blocked or needs rewrite",[254,1288],{"x1":1289,"y1":1248,"x2":1290,"y2":1248,"stroke":101,"style":1250},"388","684",[104,1292,1294],{"x":1293,"y":1254,"fill":108,"style":1255},"392","asyncpg and psycopg3 drivers",[104,1296,1297],{"x":1293,"y":1259,"fill":108,"style":1255},"redis-py client sockets",[104,1299,1300],{"x":1293,"y":1263,"fill":108,"style":1255},"SQLAlchemy async sessions",[104,1302,1303],{"x":1293,"y":1267,"fill":108,"style":1255},"Celery workers and beat",[104,1305,1306],{"x":1293,"y":119,"fill":108,"style":1255},"boto3, subprocess, threading",[104,1308,1309],{"x":1293,"y":1274,"fill":108,"style":1255},"Alembic migration runner",[104,1311,1312],{"x":1293,"y":1278,"fill":143,"style":1255},"Verdict: keep a container",[29,1314],{},[32,1316,1318],{"id":1317},"cold-starts-are-real-just-small","Cold starts are real, just small",[14,1320,1321],{},"A JavaScript Worker starts in roughly a millisecond. A Python Worker has to bring up a Pyodide interpreter and import your module graph, which is meaningfully slower — hundreds of milliseconds on a naive boot, and worse once FastAPI and Pydantic are imported. Cloudflare mitigates this with memory snapshots taken at deploy time: the interpreter state after your imports is captured once and restored per isolate, which cuts a full interpreter boot down to a fraction of it.",[14,1323,1324,1325,1329],{},"The number that matters is not the cold start in isolation but how often you pay it. Isolates stay warm while traffic flows, so a Worker serving even a few requests a minute per region rarely cold-starts. A container that scales to zero — the pattern that makes Fly.io cheap in ",[23,1326,1328],{"href":1327},"\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"," — wakes far more slowly, because it boots a machine, a Python process, and an ASGI server before it can answer.",[68,1331,77,1336,77,1339,77,1342,77,1345,77,1350,77,1354,77,1361,77,1367,77,1371,77,1375,77,1380,77,1383,77,1386,77,1391,77,1395,77,1399,77,1404,77,1407,77,1409,77,1412,77,1417],{"viewBox":1332,"role":71,"ariaLabelledBy":1333,"xmlns":75,"style":76},"0 0 720 260",[1334,1335],"cfpy-cold-t","cfpy-cold-d",[79,1337,1338],{"id":1334},"Startup latency by hosting model",[83,1340,1341],{"id":1335},"Horizontal bars comparing a warm Workers isolate, a snapshotted Python Worker cold start, an unsnapshotted boot, and a container waking from zero.",[87,1343],{"x":89,"y":89,"width":90,"height":1344,"fill":92},"260",[104,1346,1349],{"x":95,"y":1347,"fill":108,"style":1348},"28","font-size:13;font-family:var(--font-sans);","Time to first byte on a fresh instance",[104,1351,1353],{"x":95,"y":1352,"fill":108,"style":1255},"62","Warm isolate",[87,1355],{"x":1356,"y":1357,"width":1358,"height":1359,"rx":1360,"fill":131},"230","50","4","18","2",[104,1362,1366],{"x":1363,"y":1364,"fill":114,"style":1365},"246","64","font-size:11;font-family:var(--font-sans);","~5 ms",[104,1368,1370],{"x":95,"y":1369,"fill":108,"style":1255},"106","Python Worker, snapshot",[87,1372],{"x":1356,"y":1373,"width":1374,"height":1359,"rx":1360,"fill":120},"94","77",[104,1376,1379],{"x":1377,"y":1378,"fill":114,"style":1365},"319","108","~400 ms",[104,1381,1382],{"x":95,"y":97,"fill":108,"style":1255},"Python Worker, cold boot",[87,1384],{"x":1356,"y":1263,"width":1356,"height":1359,"rx":1360,"fill":1385},"var(--c-yellow)",[104,1387,1390],{"x":1388,"y":1389,"fill":114,"style":1365},"472","152","~1.2 s",[104,1392,1394],{"x":95,"y":1393,"fill":108,"style":1255},"194","Container from zero",[87,1396],{"x":1356,"y":1397,"width":1398,"height":1359,"rx":1360,"fill":143},"182","480",[104,1400,1403],{"x":1401,"y":1402,"fill":108,"style":115},"600","196","~2.5 s",[254,1405],{"x1":1356,"y1":185,"x2":1406,"y2":185,"stroke":101,"style":1250},"710",[104,1408,89],{"x":1356,"y":1356,"fill":114,"style":115},[104,1410,1411],{"x":204,"y":1356,"fill":114,"style":115},"1.25 s",[104,1413,1416],{"x":1414,"y":1356,"fill":114,"style":1415},"706","text-anchor:end;font-size:11;font-family:var(--font-sans);","2.5 s",[104,1418,1419],{"x":95,"y":194,"fill":114,"style":1365},"Warm paths dominate: the cold column only matters for the first hit per region.",[29,1421],{},[32,1423,1425],{"id":1424},"bindings-replace-your-postgres","Bindings replace your Postgres",[14,1427,1428,1429,1433],{},"Losing a socket layer sounds catastrophic until you look at what a young commercial API actually stores. Most of it is API keys, per-customer quota counters, cached upstream responses, and an append-only usage log. KV handles the first three well — reads are single-digit milliseconds from cache and writes propagate globally within seconds. Quota counters that must be exact belong in a Durable Object, which serialises writes for one key. Usage events belong in a queue that drains to durable storage, which keeps the request path fast and is the same shape recommended in ",[23,1430,1432],{"href":1431},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Ftracking-api-usage-and-analytics\u002Flogging-api-usage-events-to-postgres\u002F","logging API usage events to Postgres",".",[243,1435,1437],{"className":245,"code":1436,"language":247,"meta":248,"style":248},"# src\u002Fquota.py — quota + key auth using bindings, not a database connection\nimport os\nimport json\nfrom datetime import date\n\nFREE_DAILY = int(os.getenv(\"FREE_TIER_DAILY_QUOTA\", \"1000\"))\n\n\nasync def authorize(env, api_key: str) -> dict:\n    \"\"\"Return the customer record for an API key, or raise.\"\"\"\n    raw = await env.KEYS.get(f\"key:{api_key}\")\n    if raw is None:\n        raise PermissionError(\"unknown api key\")\n    return json.loads(raw)\n\n\nasync def charge_request(env, customer: dict, units: int = 1) -> int:\n    \"\"\"Increment today's counter in a Durable Object and return the new total.\"\"\"\n    key = f\"{customer['id']}:{date.today().isoformat()}\"\n    stub = env.QUOTA.get(env.QUOTA.idFromName(key))\n    response = await stub.fetch(\n        os.getenv(\"QUOTA_STUB_URL\", \"https:\u002F\u002Fquota.internal\u002Fincr\"),\n        method=\"POST\",\n        body=json.dumps({\"units\": units}),\n    )\n    total = int(await response.text())\n\n    match customer.get(\"plan\"):\n        case \"free\" if total > FREE_DAILY:\n            raise PermissionError(\"free tier quota exhausted\")\n        case \"metered\":\n            await env.USAGE.send({\"customer\": customer[\"id\"], \"units\": units})\n        case _:\n            pass\n    return total\n",[18,1438,1439,1444,1450,1456,1468,1472,1492,1496,1500,1521,1526,1558,1571,1585,1592,1596,1600,1630,1635,1673,1693,1704,1719,1732,1748,1753,1768,1772,1786,1808,1822,1831,1861,1868,1873],{"__ignoreMap":248},[252,1440,1441],{"class":254,"line":255},[252,1442,1443],{"class":258},"# src\u002Fquota.py — quota + key auth using bindings, not a database connection\n",[252,1445,1446,1448],{"class":254,"line":262},[252,1447,266],{"class":265},[252,1449,270],{"class":269},[252,1451,1452,1454],{"class":254,"line":273},[252,1453,266],{"class":265},[252,1455,278],{"class":269},[252,1457,1458,1460,1463,1465],{"class":254,"line":281},[252,1459,284],{"class":265},[252,1461,1462],{"class":269}," datetime ",[252,1464,266],{"class":265},[252,1466,1467],{"class":269}," date\n",[252,1469,1470],{"class":254,"line":295},[252,1471,299],{"emptyLinePlaceholder":298},[252,1473,1474,1477,1479,1482,1484,1486,1488,1490],{"class":254,"line":302},[252,1475,1476],{"class":374},"FREE_DAILY",[252,1478,640],{"class":265},[252,1480,1481],{"class":374}," int",[252,1483,470],{"class":269},[252,1485,473],{"class":338},[252,1487,342],{"class":269},[252,1489,478],{"class":338},[252,1491,348],{"class":269},[252,1493,1494],{"class":254,"line":313},[252,1495,299],{"emptyLinePlaceholder":298},[252,1497,1498],{"class":254,"line":318},[252,1499,299],{"emptyLinePlaceholder":298},[252,1501,1502,1504,1506,1509,1512,1514,1517,1519],{"class":254,"line":351},[252,1503,542],{"class":265},[252,1505,545],{"class":265},[252,1507,1508],{"class":407}," authorize",[252,1510,1511],{"class":269},"(env, api_key: ",[252,1513,375],{"class":374},[252,1515,1516],{"class":269},") -> ",[252,1518,637],{"class":374},[252,1520,648],{"class":269},[252,1522,1523],{"class":254,"line":356},[252,1524,1525],{"class":338},"    \"\"\"Return the customer record for an API key, or raise.\"\"\"\n",[252,1527,1528,1531,1533,1535,1537,1540,1542,1544,1547,1549,1552,1554,1556],{"class":254,"line":362},[252,1529,1530],{"class":269},"    raw ",[252,1532,324],{"class":265},[252,1534,674],{"class":265},[252,1536,677],{"class":269},[252,1538,1539],{"class":374},"KEYS",[252,1541,683],{"class":269},[252,1543,686],{"class":265},[252,1545,1546],{"class":338},"\"key:",[252,1548,692],{"class":374},[252,1550,1551],{"class":269},"api_key",[252,1553,698],{"class":374},[252,1555,614],{"class":338},[252,1557,437],{"class":269},[252,1559,1560,1562,1565,1567,1569],{"class":254,"line":368},[252,1561,708],{"class":265},[252,1563,1564],{"class":269}," raw ",[252,1566,714],{"class":265},[252,1568,720],{"class":374},[252,1570,648],{"class":269},[252,1572,1573,1575,1578,1580,1583],{"class":254,"line":391},[252,1574,768],{"class":265},[252,1576,1577],{"class":374}," PermissionError",[252,1579,531],{"class":269},[252,1581,1582],{"class":338},"\"unknown api key\"",[252,1584,437],{"class":269},[252,1586,1587,1589],{"class":254,"line":396},[252,1588,443],{"class":265},[252,1590,1591],{"class":269}," json.loads(raw)\n",[252,1593,1594],{"class":254,"line":401},[252,1595,299],{"emptyLinePlaceholder":298},[252,1597,1598],{"class":254,"line":423},[252,1599,299],{"emptyLinePlaceholder":298},[252,1601,1602,1604,1606,1609,1612,1614,1617,1619,1621,1624,1626,1628],{"class":254,"line":440},[252,1603,542],{"class":265},[252,1605,545],{"class":265},[252,1607,1608],{"class":407}," charge_request",[252,1610,1611],{"class":269},"(env, customer: ",[252,1613,637],{"class":374},[252,1615,1616],{"class":269},", units: ",[252,1618,467],{"class":374},[252,1620,640],{"class":265},[252,1622,1623],{"class":374}," 1",[252,1625,1516],{"class":269},[252,1627,467],{"class":374},[252,1629,648],{"class":269},[252,1631,1632],{"class":254,"line":449},[252,1633,1634],{"class":338},"    \"\"\"Increment today's counter in a Durable Object and return the new total.\"\"\"\n",[252,1636,1637,1640,1642,1645,1647,1649,1652,1655,1658,1660,1663,1665,1668,1670],{"class":254,"line":458},[252,1638,1639],{"class":269},"    key ",[252,1641,324],{"class":265},[252,1643,1644],{"class":265}," f",[252,1646,614],{"class":338},[252,1648,692],{"class":374},[252,1650,1651],{"class":269},"customer[",[252,1653,1654],{"class":338},"'id'",[252,1656,1657],{"class":269},"]",[252,1659,698],{"class":374},[252,1661,1662],{"class":338},":",[252,1664,692],{"class":374},[252,1666,1667],{"class":269},"date.today().isoformat()",[252,1669,698],{"class":374},[252,1671,1672],{"class":338},"\"\n",[252,1674,1675,1678,1680,1682,1685,1688,1690],{"class":254,"line":484},[252,1676,1677],{"class":269},"    stub ",[252,1679,324],{"class":265},[252,1681,677],{"class":269},[252,1683,1684],{"class":374},"QUOTA",[252,1686,1687],{"class":269},".get(env.",[252,1689,1684],{"class":374},[252,1691,1692],{"class":269},".idFromName(key))\n",[252,1694,1695,1697,1699,1701],{"class":254,"line":509},[252,1696,825],{"class":269},[252,1698,324],{"class":265},[252,1700,674],{"class":265},[252,1702,1703],{"class":269}," stub.fetch(\n",[252,1705,1706,1709,1712,1714,1717],{"class":254,"line":515},[252,1707,1708],{"class":269},"        os.getenv(",[252,1710,1711],{"class":338},"\"QUOTA_STUB_URL\"",[252,1713,342],{"class":269},[252,1715,1716],{"class":338},"\"https:\u002F\u002Fquota.internal\u002Fincr\"",[252,1718,506],{"class":269},[252,1720,1721,1724,1726,1729],{"class":254,"line":520},[252,1722,1723],{"class":330},"        method",[252,1725,324],{"class":265},[252,1727,1728],{"class":338},"\"POST\"",[252,1730,1731],{"class":269},",\n",[252,1733,1734,1737,1739,1742,1745],{"class":254,"line":525},[252,1735,1736],{"class":330},"        body",[252,1738,324],{"class":265},[252,1740,1741],{"class":269},"json.dumps({",[252,1743,1744],{"class":338},"\"units\"",[252,1746,1747],{"class":269},": units}),\n",[252,1749,1750],{"class":254,"line":539},[252,1751,1752],{"class":269},"    )\n",[252,1754,1755,1758,1760,1762,1764,1766],{"class":254,"line":562},[252,1756,1757],{"class":269},"    total ",[252,1759,324],{"class":265},[252,1761,1481],{"class":374},[252,1763,531],{"class":269},[252,1765,916],{"class":265},[252,1767,919],{"class":269},[252,1769,1770],{"class":254,"line":591},[252,1771,299],{"emptyLinePlaceholder":298},[252,1773,1774,1777,1780,1783],{"class":254,"line":596},[252,1775,1776],{"class":265},"    match",[252,1778,1779],{"class":269}," customer.get(",[252,1781,1782],{"class":338},"\"plan\"",[252,1784,1785],{"class":269},"):\n",[252,1787,1788,1791,1794,1797,1800,1803,1806],{"class":254,"line":601},[252,1789,1790],{"class":265},"        case",[252,1792,1793],{"class":338}," \"free\"",[252,1795,1796],{"class":265}," if",[252,1798,1799],{"class":269}," total ",[252,1801,1802],{"class":265},">",[252,1804,1805],{"class":374}," FREE_DAILY",[252,1807,648],{"class":269},[252,1809,1810,1813,1815,1817,1820],{"class":254,"line":619},[252,1811,1812],{"class":265},"            raise",[252,1814,1577],{"class":374},[252,1816,531],{"class":269},[252,1818,1819],{"class":338},"\"free tier quota exhausted\"",[252,1821,437],{"class":269},[252,1823,1824,1826,1829],{"class":254,"line":651},[252,1825,1790],{"class":265},[252,1827,1828],{"class":338}," \"metered\"",[252,1830,648],{"class":269},[252,1832,1833,1836,1838,1841,1844,1847,1850,1853,1856,1858],{"class":254,"line":666},[252,1834,1835],{"class":265},"            await",[252,1837,677],{"class":269},[252,1839,1840],{"class":374},"USAGE",[252,1842,1843],{"class":269},".send({",[252,1845,1846],{"class":338},"\"customer\"",[252,1848,1849],{"class":269},": customer[",[252,1851,1852],{"class":338},"\"id\"",[252,1854,1855],{"class":269},"], ",[252,1857,1744],{"class":338},[252,1859,1860],{"class":269},": units})\n",[252,1862,1863,1865],{"class":254,"line":705},[252,1864,1790],{"class":265},[252,1866,1867],{"class":269}," _:\n",[252,1869,1870],{"class":254,"line":725},[252,1871,1872],{"class":265},"            pass\n",[252,1874,1875,1877],{"class":254,"line":734},[252,1876,443],{"class":265},[252,1878,1879],{"class":269}," total\n",[14,1881,1882,1883,1885,1886,1890],{},"The ",[18,1884,1840],{}," queue consumer can run as a second Worker or as a container job that writes to your real warehouse. That split is the honest architecture: the hot path stays at the edge, the analytical path stays where SQL lives. Authentication logic is unchanged — the trade-off between signed tokens and stored keys in ",[23,1887,1889],{"href":1888},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002Fjwt-vs-api-keys-for-python-apis\u002F","JWT vs API keys"," applies identically, and a JWT check is actually the better fit because it avoids a KV read per request.",[29,1892],{},[32,1894,1896],{"id":1895},"when-to-choose-workers-when-to-stay-on-a-container","When to choose Workers, when to stay on a container",[14,1898,1899,1900,1904],{},"Choose Workers when your API is read-heavy, latency-sensitive, globally distributed, and thin on dependencies: an enrichment endpoint, a signed-URL issuer, a geolocation or feature-flag lookup, a webhook receiver that validates a signature and enqueues work. At 1M requests a month with 4 ms of CPU each, you sit inside the paid plan's included allowance and pay about five dollars — versus seven to twenty-five for a small always-on container plus a managed database. Run your own numbers using ",[23,1901,1903],{"href":1902},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request"," before you assume the edge is cheaper; it usually is, until you need Postgres.",[14,1906,1907,1908,1912,1913,1917],{},"Stay on a container when you have a real relational schema with migrations, a background job system, long-running work that exceeds the CPU budget, machine-learning dependencies with native wheels, or a team that needs the local-dev parity a ",[23,1909,1911],{"href":1910},"\u002Fscaling-and-operating-production-python-apis\u002Fcontainerizing-python-apis-with-docker\u002F","Docker image"," gives you. Anything that looks like ",[23,1914,1916],{"href":1915},"\u002Fscaling-and-operating-production-python-apis\u002Frunning-background-jobs-with-celery\u002F","background jobs with Celery"," is a container workload, full stop.",[68,1919,77,1924,77,1927,77,1930,77,1933,77,1936,77,1940,77,1943,77,1947,77,1949,77,1953,77,1956,77,1958,77,1961,77,1965,77,1968,77,1971,77,1975,77,1978,77,1982,77,1985,77,1988,77,1991,77,1995,77,1998,77,2002,77,2005,77,2009,77,2013,77,2017,77,2021,77,2024,77,2028],{"viewBox":1920,"role":71,"ariaLabelledBy":1921,"xmlns":75,"style":76},"0 0 720 330",[1922,1923],"cfpy-tree-t","cfpy-tree-d",[79,1925,1926],{"id":1922},"Decision tree for Workers versus a container host",[83,1928,1929],{"id":1923},"Three questions about native drivers, relational schema and background jobs route an API to Cloudflare Python Workers, a hybrid split, or a container host.",[87,1931],{"x":89,"y":89,"width":90,"height":1932,"fill":92},"330",[87,1934],{"x":1935,"y":95,"width":1935,"height":96,"rx":99,"fill":100,"stroke":101,"style":102},"240",[104,1937,1939],{"x":173,"y":1938,"fill":108,"style":109},"45","Does it need a native driver?",[152,1941],{"d":1942,"stroke":114,"style":102},"M360 62 L360 86",[104,1944,1946],{"x":1945,"y":107,"fill":114,"style":1365},"374","no",[87,1948],{"x":1935,"y":1254,"width":1935,"height":96,"rx":99,"fill":100,"stroke":101,"style":102},[104,1950,1952],{"x":173,"y":1951,"fill":108,"style":109},"115","Relational schema required?",[152,1954],{"d":1955,"stroke":114,"style":102},"M360 132 L360 156",[104,1957,1946],{"x":1945,"y":97,"fill":114,"style":1365},[87,1959],{"x":1935,"y":1960,"width":1935,"height":96,"rx":99,"fill":100,"stroke":101,"style":102},"156",[104,1962,1964],{"x":173,"y":1963,"fill":108,"style":109},"185","Long background jobs?",[152,1966],{"d":1967,"stroke":131,"style":102},"M360 202 L360 236",[104,1969,1946],{"x":1945,"y":1970,"fill":114,"style":1365},"224",[87,1972],{"x":1356,"y":1973,"width":1344,"height":1974,"rx":99,"fill":92,"stroke":131,"style":102},"236","54",[104,1976,1977],{"x":173,"y":1344,"fill":108,"style":109},"Ship it on Python Workers",[104,1979,1981],{"x":173,"y":1980,"fill":114,"style":115},"280","KV, D1 and queues cover state",[152,1983],{"d":1984,"stroke":143,"fill":164,"style":102},"M240 39 L120 39 L120 236",[152,1986],{"d":1987,"stroke":143,"fill":164,"style":102},"M240 109 L120 109",[152,1989],{"d":1990,"stroke":143,"fill":164,"style":102},"M240 179 L120 179",[104,1992,1994],{"x":97,"y":1993,"fill":114,"style":1365},"32","yes",[87,1996],{"x":95,"y":1973,"width":1997,"height":1974,"rx":99,"fill":92,"stroke":143,"style":102},"200",[104,1999,2001],{"x":2000,"y":1344,"fill":108,"style":109},"116","Container host",[104,2003,2004],{"x":2000,"y":1980,"fill":114,"style":115},"Render, Railway, Fly",[87,2006],{"x":2007,"y":1960,"width":1393,"height":2008,"rx":99,"fill":92,"stroke":120,"style":102},"510","90",[104,2010,2012],{"x":2011,"y":179,"fill":108,"style":109},"607","Hybrid split",[104,2014,2016],{"x":2011,"y":2015,"fill":114,"style":115},"208","edge reads on Workers",[104,2018,2020],{"x":2011,"y":2019,"fill":114,"style":115},"228","writes on the container",[152,2022],{"d":2023,"stroke":120,"fill":164,"style":102},"M480 109 L607 109 L607 154",[104,2025,2027],{"x":2026,"y":113,"fill":114,"style":1365},"498","yes, but read-heavy",[104,2029,2031],{"x":173,"y":2030,"fill":114,"style":115},"316","Most first commercial APIs land on the hybrid split within a year.",[29,2033],{},[32,2035,2037],{"id":2036},"the-migration-path-that-does-not-burn-a-weekend","The migration path that does not burn a weekend",[14,2039,2040],{},"Do not port the whole app. Move one endpoint, measure, then decide.",[2042,2043,2044,2052,2074,2077,2087],"ol",{},[2045,2046,2047,2048,2051],"li",{},"Create the Worker with ",[18,2049,2050],{},"npx wrangler init --template hello-world-python"," and copy in your smallest read-only route.",[2045,2053,2054,2055,2058,2059,2061,2062,2065,2066,2069,2070,2073],{},"Add ",[18,2056,2057],{},"fastapi"," to ",[18,2060,1102],{},", set ",[18,2063,2064],{},"compatibility_flags"," to include ",[18,2067,2068],{},"python_workers",", and run ",[18,2071,2072],{},"npx wrangler dev"," locally against real bindings.",[2045,2075,2076],{},"Replace every direct database read on that route with a KV or D1 call; keep the Pydantic response models untouched so your contract does not move.",[2045,2078,2079,2080,2083,2084,1433],{},"Push secrets with ",[18,2081,2082],{},"npx wrangler secret put"," — never commit them — and deploy to a subdomain like ",[18,2085,2086],{},"edge.your-api.com",[2045,2088,2089,2090,1433],{},"Route a slice of traffic there with a Cloudflare rule, watch p95 and error rate through Workers observability, and widen the slice only when it holds. The safety mechanics are the same as any ",[23,2091,2093],{"href":2092},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fzero-downtime-deploys-for-python-apis\u002F","zero-downtime deploy",[14,2095,2096],{},"Keep your container running the entire time. The hybrid state — edge Worker in front, container behind for writes and jobs — is a legitimate destination, not a halfway house, and it is where most profitable small APIs end up.",[29,2098],{},[32,2100,2102],{"id":2101},"builder-verdict","Builder verdict",[14,2104,2105,2106,2110],{},"For your first or second commercial API, the container host wins, and Cloudflare Python Workers wins the layer in front of it. Ship the product on Render or Fly where Postgres, migrations, Celery, and ",[23,2107,2109],{"href":2108},"\u002Fscaling-and-operating-production-python-apis\u002Fmonitoring-and-logging-python-apis\u002F","structured logging"," all work the way you already know, because shipping velocity is worth far more than saving fifteen dollars a month. Then move the read-heavy, latency-sensitive endpoints — lookups, validators, webhook receivers, cache fronts — onto a Python Worker where they cost effectively nothing and answer in the customer's own city. Betting the whole product on Pyodide today means rewriting your data layer for D1 to save a rounding error on your hosting bill, and that trade only pays off once traffic is large enough that per-request compute genuinely shows up in your margin.",[29,2112],{},[32,2114,2116],{"id":2115},"faq","FAQ",[14,2118,2119,2123],{},[2120,2121,2122],"strong",{},"Is Cloudflare Python Workers cheaper than a container at 1M requests a month?","\nUsually yes on compute alone. The Workers Paid plan starts at five dollars and includes an allowance that a 1M-request-per-month API with a few milliseconds of CPU per request stays inside, while a small always-on container plus managed Postgres runs fifteen to thirty dollars. The saving evaporates if you still need that Postgres for writes, so compare total architecture cost, not the compute line.",[14,2125,2126,2129],{},[2120,2127,2128],{},"Will my existing FastAPI app deploy without changes?","\nOnly if its dependency tree is pure Python or already bundled in the runtime. Routing, dependencies, and Pydantic models carry over untouched, but any asyncpg, psycopg, redis-py, boto3, or Celery import fails because isolates have no sockets, threads, or processes. Run a dry-run deploy with your real requirements file before you plan the migration.",[14,2131,2132,2135],{},[2120,2133,2134],{},"How risky is committing my data layer to D1 and KV?","\nModerate and one-directional. D1 speaks SQLite SQL, so simple schemas port back to Postgres with modest effort, but KV's eventual consistency and Durable Object patterns have no direct equivalent on a container. Keep the customer-of-record data in a portable relational store and use edge storage for caches, quotas, and keys you can rebuild.",[14,2137,2138,2141],{},[2120,2139,2140],{},"Do cold starts hurt paying customers?","\nRarely. Isolates stay warm while traffic flows, and Cloudflare's deploy-time memory snapshots cut Python startup to roughly 400 ms in the worst case. Enterprise-grade latency guarantees are a different conversation, but for a metered API the cold path is a fraction of a percent of requests.",[14,2143,2144,2147],{},[2120,2145,2146],{},"Can I run scheduled jobs and billing sync on Workers?","\nShort ones, yes, using Cron Triggers and Queues. Anything that needs minutes of CPU, a long database transaction, or a retry framework belongs on a container worker. Keep billing reconciliation and invoice generation off the edge — those jobs want a real process and a real database.",[32,2149,2151],{"id":2150},"related","Related",[14,2153,2154],{},[2120,2155,2156],{},"Same track:",[2158,2159,2160,2165,2170],"ul",{},[2045,2161,2162,2164],{},[23,2163,26],{"href":25}," — the parent guide covering hosting choices end to end.",[2045,2166,2167,2169],{},[23,2168,1328],{"href":1327}," — the container hosts you should compare against before going all-in on the edge.",[2045,2171,2172,2176],{},[23,2173,2175],{"href":2174},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fintegrating-stripe-with-python-apis\u002Fbest-platforms-to-host-python-apis-for-free\u002F","Best Platforms to Host Python APIs for Free"," — where a zero-budget prototype can live before it earns revenue.",[14,2178,2179],{},[2120,2180,2181],{},"Other tracks:",[2158,2183,2184,2190],{},[2045,2185,2186,2189],{},[23,2187,2188],{"href":1910},"Containerizing Python APIs with Docker"," — the fallback runtime for everything Pyodide cannot import.",[2045,2191,2192,2195],{},[23,2193,2194],{"href":1902},"Calculating Cost per API Request"," — turn edge versus container pricing into a real margin number.",[2197,2198,2199],"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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":248,"searchDepth":262,"depth":262,"links":2201},[2202,2203,2204,2205,2206,2207,2208,2209,2210,2211],{"id":34,"depth":262,"text":35},{"id":224,"depth":262,"text":225},{"id":1112,"depth":262,"text":1113},{"id":1317,"depth":262,"text":1318},{"id":1424,"depth":262,"text":1425},{"id":1895,"depth":262,"text":1896},{"id":2036,"depth":262,"text":2037},{"id":2101,"depth":262,"text":2102},{"id":2115,"depth":262,"text":2116},{"id":2150,"depth":262,"text":2151},"Run FastAPI on Cloudflare Python Workers: ASGI support, Pyodide dependency limits, cold starts, KV and D1 bindings instead of Postgres, and when to stay on a container.","md",{"pageTitle":5,"type":2215,"datePublished":2216,"dateModified":2216},"article","2026-07-23","\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fdeploying-fastapi-to-cloudflare-workers-with-python",{"title":5,"description":2212},"building-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fdeploying-fastapi-to-cloudflare-workers-with-python\u002Findex","EsFhoY0ycduRFT3wQVv66JaNaNoWLZBsA3vIJXtLl-I",{"@context":2222,"@type":2223,"mainEntity":2224},"https:\u002F\u002Fschema.org","FAQPage",[2225,2230,2233,2236,2239],{"@type":2226,"name":2122,"acceptedAnswer":2227},"Question",{"@type":2228,"text":2229},"Answer","Usually yes on compute alone. The Workers Paid plan starts at five dollars and includes an allowance that a 1M-request-per-month API with a few milliseconds of CPU per request stays inside, while a small always-on container plus managed Postgres runs fifteen to thirty dollars. The saving evaporates if you still need that Postgres for writes, so compare total architecture cost, not the compute line.",{"@type":2226,"name":2128,"acceptedAnswer":2231},{"@type":2228,"text":2232},"Only if its dependency tree is pure Python or already bundled in the runtime. Routing, dependencies, and Pydantic models carry over untouched, but any asyncpg, psycopg, redis-py, boto3, or Celery import fails because isolates have no sockets, threads, or processes. Run a dry-run deploy with your real requirements file before you plan the migration.",{"@type":2226,"name":2134,"acceptedAnswer":2234},{"@type":2228,"text":2235},"Moderate and one-directional. D1 speaks SQLite SQL, so simple schemas port back to Postgres with modest effort, but KV's eventual consistency and Durable Object patterns have no direct equivalent on a container. Keep the customer-of-record data in a portable relational store and use edge storage for caches, quotas, and keys you can rebuild.",{"@type":2226,"name":2140,"acceptedAnswer":2237},{"@type":2228,"text":2238},"Rarely. Isolates stay warm while traffic flows, and Cloudflare's deploy-time memory snapshots cut Python startup to roughly 400 ms in the worst case. Enterprise-grade latency guarantees are a different conversation, but for a metered API the cold path is a fraction of a percent of requests.",{"@type":2226,"name":2146,"acceptedAnswer":2240},{"@type":2228,"text":2241},"Short ones, yes, using Cron Triggers and Queues. Anything that needs minutes of CPU, a long database transaction, or a retry framework belongs on a container worker. Keep billing reconciliation and invoice generation off the edge — those jobs want a real process and a real database.",1784887028359]