Why Flask's Built-in Server Isn't for Production
app.run() starts Werkzeug's development server, which is single-threaded by default, lacks proper process management, and explicitly warns in its own output that it is not a production WSGI server. It cannot efficiently handle concurrent requests, doesn't restart crashed workers, and has no built-in protection against slow clients tying up the process, all of which matter under real traffic.
Cricket analogy: A club-level net bowler can deliver balls fine for casual practice but isn't built to bowl 90 overs in a five-day Test match; app.run() is that net bowler, not a production-grade server like Gunicorn.
WSGI: The Interface Between Server and Application
WSGI (Web Server Gateway Interface, PEP 3333) defines a simple callable contract: a WSGI application is any callable that accepts environ (a dict of request data) and start_response (a callback to set status and headers), and returns an iterable of bytes. Flask's app object is itself a WSGI application, which is exactly why Gunicorn can call app:app directly, and why Flask apps are compatible with any WSGI-compliant server without modification.
Cricket analogy: The Laws of Cricket define a standard interface every ground must follow, so a bowler trained in Mumbai can play a Test at Lord's without relearning the rules, just as WSGI lets any compliant server run any compliant app.
Configuring Gunicorn for a Flask Application
Gunicorn is invoked as gunicorn -w 4 -b 0.0.0.0:8000 myapp:app, where -w 4 spawns four worker processes and myapp:app tells Gunicorn to import the app object from myapp.py. Gunicorn's default sync worker handles one request per worker at a time, which is fine for CPU-bound or fast I/O-bound views, but for apps with slow external calls, gevent or gthread workers let a single worker juggle many concurrent connections.
Cricket analogy: A T20 franchise fields eleven specialist players simultaneously across different positions rather than relying on one all-rounder to cover everything, the way Gunicorn's four workers handle requests in parallel instead of one process doing it all.
Worker Types and Sizing Guidance
The commonly cited formula for sync workers is (2 x CPU cores) + 1, which balances CPU utilization against the overhead of context switching. For apps that spend most of their time waiting on external I/O (database queries, third-party APIs), gthread workers with a higher --threads count, or async gevent/eventlet workers, allow far more concurrent connections per worker than the sync default without needing dozens of OS processes.
Cricket analogy: A captain sets a fielding formation based on the bowler and pitch conditions rather than always using the same setup, just as worker count and type should be tuned to the app's CPU vs I/O profile.
# Install
pip install gunicorn
# Run with 4 sync workers on port 8000
gunicorn -w 4 -b 0.0.0.0:8000 myapp:app
# Run with gevent workers for I/O-heavy apps (e.g. lots of DB/API calls)
pip install gevent
gunicorn -k gevent -w 4 --threads 4 -b 0.0.0.0:8000 myapp:create_app()
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "gthread"
threads = 4
timeout = 30
accesslog = "-"
errorlog = "-"
myapp:app tells Gunicorn to import the module myapp and use its app attribute. If you use the application factory pattern, point Gunicorn at the factory call instead, e.g. myapp:create_app().
Gunicorn's built-in workers are not designed to serve directly to the public internet efficiently — it should always sit behind a reverse proxy like Nginx that handles TLS termination, static files, and buffering slow clients.
- Flask's app.run() dev server is single-threaded and explicitly unsuitable for production traffic.
- WSGI (PEP 3333) is the standard callable interface that lets any WSGI server host any WSGI app, including Flask.
- Gunicorn is a production WSGI server that manages multiple worker processes for concurrency.
- Sync workers suit CPU-bound apps; gevent/gthread workers suit I/O-bound apps with many external calls.
- A common sizing formula for sync workers is (2 x CPU cores) + 1.
- myapp:app or myapp:create_app() tells Gunicorn where to find the WSGI-callable Flask app.
- Gunicorn should run behind a reverse proxy like Nginx for TLS, static files, and buffering.
Practice what you learned
1. Why is Flask's built-in app.run() server unsuitable for production?
2. What does WSGI standardize?
3. Which Gunicorn worker type is generally best for an app that spends most of its time waiting on slow third-party API calls?
4. In the command 'gunicorn -w 4 -b 0.0.0.0:8000 myapp:app', what does 'myapp:app' refer to?
5. Why should Gunicorn typically run behind a reverse proxy like Nginx?
Was this page helpful?
You May Also Like
Deploying a Flask App
A practical guide to taking a Flask application from local development to a secure, production deployment using Docker, environment configuration, and health checks.
Testing Flask Applications
Learn how to write reliable unit and integration tests for Flask apps using pytest, the Flask test client, and fixtures for database and app context.
Flask Quick Reference
A condensed cheat sheet covering Flask routing, request/response handling, context objects, and CLI essentials for quick lookup while coding.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics