Python — Flask Development
Flask Deployment with Gunicorn and Nginx
Direct answer
The production-standard Flask deployment is gunicorn as the WSGI server — multiple workers, a firm timeout, and max_requests recycling — behind nginx, which terminates TLS, buffers slow clients, and serves static files. Add Werkzeug's ProxyFix so Flask trusts the forwarded headers for client IP and scheme, run gunicorn under systemd for supervision, and you have a deployment that stays boring for years.
This stack is old, documented, and unfashionable, which is exactly why I keep shipping it — it fails in known ways. Here's how I configure each layer and the specific settings that separate a tutorial deployment from one that survives real traffic.
Key facts, with sources
- In the JetBrains Python Developers Survey 2024, Flask was used by 34% of Python developers, statistically neck and neck with Django at 35% and just behind FastAPI at 38%. (JetBrains Python Developers Survey 2024)
- The 2025 Stack Overflow Developer Survey recorded Flask at 14.4% of respondents, nearly tied with FastAPI at 14.8% and ahead of Django at 12.6%. (Stack Overflow Developer Survey 2025)
- The Flask project shipped only two releases during all of 2025, both patch releases to version 3.1.0 from November 2024, reflecting a mature and stable codebase rather than rapid feature churn. (miguelgrinberg.com)
- FastAPI overtook Flask in GitHub stars for the first time in December 2025, at roughly 88,000 stars versus Flask's 68,400, after years of Flask holding the lead. (DZone)
- Published benchmark comparisons show roughly a 5x throughput gap in FastAPI's favor, with a Flask application on Gunicorn typically handling about 2,000 to 3,000 requests per second on simple endpoints. (Strapi)
Why three layers, and what each one is for
Flask's built-in server is a development convenience: single-threaded, unhardened, and explicit in its own docs that it shouldn't face traffic. Gunicorn provides the process model — a master supervising worker processes, restarting them on crash, giving you real concurrency on a WSGI app.
Nginx in front earns its place three ways: it terminates TLS, it serves static assets without waking Python, and above all it buffers slow clients. Without buffering, a client trickling a request over a slow connection occupies an entire gunicorn worker for the duration; nginx absorbs the trickle and hands gunicorn a complete request. That single property is why the proxy layer matters even for small deployments.
The gunicorn settings that actually matter
Worker count starts near twice the CPU cores plus one, then gets adjusted by measurement — memory per worker is usually the binding constraint before CPU is. The timeout setting is your protection against stuck requests: a worker exceeding it gets killed and replaced, which converts a creeping outage into a logged error.
max_requests with jitter quietly recycles workers after a set number of requests, which contains slow memory leaks — not a fix, but a reliable mitigation while you hunt the leak. Log to stdout and let the process manager handle log routing.
bind = "unix:/run/gunicorn/app.sock"
workers = 5 # start near (2 x CPU cores) + 1, then measure
worker_class = "gthread"
threads = 4
timeout = 30 # kill and replace workers stuck past this
graceful_timeout = 30
keepalive = 5
max_requests = 1000 # recycle workers to contain slow leaks
max_requests_jitter = 100
accesslog = "-"
errorlog = "-"Choosing a worker class without folklore
Sync workers are the default and remain right for fast, CPU-light request handling: one request per worker, no shared-state surprises, trivially debuggable. gthread adds a thread pool per worker, multiplying concurrency for requests that spend time waiting on the database or external APIs — it's my usual choice for typical CRUD services because the win is real and the complexity is small.
gevent goes further, monkey-patching the standard library for cooperative concurrency, and can hold thousands of connections — but it demands that every driver in your stack cooperates, and the failure modes when something blocks are subtle. I reach for it only with a demonstrated need, and at that point FastAPI is often the more honest answer.
The nginx server block
On a single host, connect nginx to gunicorn over a unix socket — it skips the TCP stack and avoids port management. The forwarded headers are not decoration: X-Forwarded-For and X-Forwarded-Proto are how Flask will learn the real client IP and scheme. client_max_body_size should match your actual upload ceiling, because the default is small and the resulting 413s confuse everyone downstream.
upstream flask_app {
server unix:/run/gunicorn/app.sock fail_timeout=0;
}
server {
listen 80;
server_name _;
client_max_body_size 10m;
location /static/ {
alias /srv/app/static/;
expires 30d;
}
location / {
proxy_pass http://flask_app;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}ProxyFix, systemd, and graceful restarts
Behind a proxy, Flask sees every request as plain HTTP from the proxy's IP. The symptoms are sneaky: url_for generates http links, rate limits key on one IP, secure cookies refuse to set. Werkzeug's ProxyFix middleware fixes all of it — wrap the WSGI app and tell it exactly how many proxies to trust, and never more, since over-trusting forwarded headers lets clients spoof their IP.
Run gunicorn under systemd with Restart=always so crashes self-heal, and reload with a HUP signal — gunicorn finishes in-flight requests on old workers while booting new ones, giving you zero-dropped-request deploys on a single box. A lightweight health endpoint closes the loop for monitoring.
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)When to hire senior help
Senior help is most valuable for Flask when an app built as a prototype is now carrying production traffic: an experienced engineer can add proper WSGI serving, task queues, and test coverage without a rewrite. Also consider it before committing to a Flask-to-FastAPI migration, since an expert assessment often shows targeted fixes deliver the needed performance at a fraction of the cost. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Python — Flask Development projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Running Flask's built-in development server in production instead of Gunicorn or uWSGI behind a reverse proxy
- ✕Storing per-request state in module-level globals or misusing the application context, causing race conditions once multiple workers or threads are enabled
- ✕Assembling auth, ORM, and validation from third-party Flask extensions without checking maintenance status, then inheriting abandoned dependencies
- ✕Executing long-running work (PDF generation, email, external API calls) inside request handlers instead of a task queue, exhausting workers and triggering gateway timeouts
Frequently asked questions
How many gunicorn workers should I run for a Flask app?
Start near (2 × CPU cores) + 1 for sync workers, then adjust by measurement — memory per worker usually becomes the limit before CPU does. If requests spend significant time waiting on the database or external APIs, use the gthread worker class and add threads per worker to multiply concurrency without more processes.
Do I still need nginx if my cloud load balancer terminates TLS?
Often the load balancer covers TLS and some buffering, and on a PaaS you can skip nginx entirely. On your own instances, nginx still earns its place by buffering slow clients so they don't pin gunicorn workers, serving static files without invoking Python, and giving you request-level controls like body-size limits close to the app.
Why does my Flask app see the wrong client IP behind nginx?
Because Flask sees the proxy's connection, not the client's — the real IP travels in the X-Forwarded-For header. Set the forwarded headers in your nginx location block, then wrap the app with Werkzeug's ProxyFix configured for exactly one trusted hop. That also fixes scheme detection, so url_for and secure cookies behave correctly.
Is Flask outdated now that FastAPI is more popular?
No. Flask still shows 34% usage in the JetBrains 2024 survey and 14.4% in Stack Overflow 2025, and its slow release cadence reflects stability, not abandonment. It remains a strong choice for server-rendered apps, internal tools, and teams that value its minimal, well-documented core.
Can Flask scale to serious production traffic?
Yes, with the standard pattern of Gunicorn workers behind a load balancer plus caching; benchmark figures of 2,000 to 3,000 requests per second per instance are before horizontal scaling. Most products hit database and architecture limits long before Flask itself is the bottleneck.
Should we migrate an existing Flask app to FastAPI?
Only if you have a concrete driver such as high-concurrency I/O workloads, a need for typed request validation, or mandatory OpenAPI docs. A rewrite of a working Flask app rarely pays back; many teams instead add new async services alongside the existing Flask core.
Bottom line: Dhairya Senjaliya ships Python — Flask Development projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.