Which production server fits a low-traffic Flask app: Gunicorn with gthread, Uvicorn, or serverless?
0 reputation · 12 Jul 2021, 06:09 UTC
0 reputation · 12 Jul 2021, 06:09 UTC
I'm deploying a low-traffic Flask application (a few requests per minute at most) and want to minimize hosting costs without sacrificing correctness. The Flask documentation lists several deployment options but does not prescribe one, so the choice is left open.
The candidates I'm weighing are: Gunicorn with a single gthread worker, Uvicorn with async views on Flask 2.0+, and a serverless option such as AWS Lambda via Zappa. My endpoints are mostly I/O-bound (database calls and external APIs), and I rely on a few third-party extensions whose async compatibility I haven't audited. I also use before_request and teardown_request hooks, and I've read that these may behave differently under an ASGI lifespan model.
Constraints: I want to avoid the development server, keep memory footprint low, and would like to verify the decision with a benchmark rather than assumed behavior.
Specific questions:
35775 reputation · 12 Jul 2021, 09:41 UTC
For a low‑traffic, I/O‑bound Flask app that uses before_request/teardown_request hooks and third‑party extensions you haven't audited for async safety, Gunicorn with a single gthread worker is the safest default. It runs on Flask's native WSGI interface, guarantees hook and extension compatibility, and keeps memory predictable. Uvicorn adds an ASGI layer that still executes synchronous code in a thread pool, so you gain no true end‑to‑end async unless every downstream dependency (DB driver, HTTP client) is async‑native. Serverless (AWS Lambda via Zappa) removes idle cost but introduces cold‑start latency, hard payload/timeouts, and packaging constraints that often break Flask assumptions.
At a few requests per minute, the resident memory difference between one Gunicorn gthread worker (e.g., --worker-class=gthread --threads=4) and one Uvicorn process is small — typically tens of megabytes. The gap only becomes meaningful under sustained concurrency. Verify with your actual app:
# Gunicorn gthread
gunicorn -w 1 -k gthread --threads 4 myapp:app &
PID=$!
sleep 2
ps -o rss= -p $PID
kill $PID
# Uvicorn
uvicorn myapp:app --workers 1 &
PID=$!
sleep 2
ps -o rss= -p $PID
kill $PID
Run each under a realistic low‑rate load (1–10 req/min with occasional bursts) using wrk or locust and compare RSS, p50/p99 latency.
There is no automated compatibility scanner. The reliable check is running your full test suite (or a smoke script hitting every endpoint) under the ASGI server. Typical risk patterns:
threading.local or greenlet for request‑scoped state.psycopg2, mysqlclient) or blocking HTTP clients (requests) called from async views — these stall the event loop and perform worse than gthread.Quick audit commands:
grep -r "threading\.local" . --include="*.py"
grep -r "Thread(" . --include="*.py"
grep -r "import psycopg2\|import mysqlclient\|import requests" . --include="*.py"
Lambda via Zappa eliminates idle billing but adds:
/tmp. Apps assuming a filesystem or long‑lived processes break.If your traffic is truly near zero and you can tolerate cold starts, serverless is cheapest. Otherwise, a small VM (e.g., 1 vCPU/1 GB RAM) running Gunicorn/gthread costs a few dollars/month and avoids these limits. Note that AWS quotas change over time — verify current payload and timeout limits before committing.
What is the largest request or response payload your app handles? If it exceeds ~5 MB, API Gateway limits will likely break Lambda/Zappa, making Gunicorn the only viable option.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.