Run Gunicorn gevent Workers for I/O-Heavy Django Apps
Use Gunicorn gevent workers for Django apps that spend time waiting on external APIs: worker_connections, monkey patching, psycopg and database connection limits, CPU-bound pitfalls, and comparing gevent with threads and ASGI.
Problem statement
A Django app whose views call slow external services (payment gateways, partner APIs, webhooks, long polling) ties up a sync Gunicorn worker for every waiting request. With four workers, five slow calls in flight make the whole site queue. You could rewrite views as async, but that is a large change. Gunicorn's gevent worker class makes existing synchronous code cooperative: while one request waits on the network, the same process serves others, without changing view code. It also brings real pitfalls around database connections and CPU-bound work.
pip install geventand rungunicorn config.wsgi -k gevent --worker-connections 100 --workers 2.- Gunicorn monkey-patches the standard library when the gevent worker starts; make sure nothing imports
sslorsocketusers before that (avoidpreload_appwith gevent unless you patch first). - Use a psycopg version and setup that cooperates with gevent (psycopg 3 works with patched sockets via its pure or C implementation when used carefully, or use psycogreen for psycopg2).
- Cap database connections: concurrent greenlets times connections can exceed PostgreSQL's limit; use PgBouncer.
- Keep CPU-heavy work out: one CPU-bound request blocks every greenlet in that worker.
- Measure: compare throughput and latency under realistic slow-I/O load against
gthreadworkers.
Step-by-step solution
1. Install and run
# gunicorn.conf.py
worker_class = "gevent"
workers = 2 # roughly CPU cores
worker_connections = 100 # max concurrent greenlets per worker
timeout = 60
pip install gevent
gunicorn -c gunicorn.conf.py config.wsgi:application
Each worker can now handle up to 100 concurrent requests, switching whenever one blocks on network I/O.
2. Monkey patching
The gevent worker calls gevent.monkey.patch_all() early so socket, ssl, time.sleep, and select become cooperative. Libraries imported before patching keep the original blocking versions. With preload_app = True, Django is imported in the master before the worker patches, which can cause hangs or RecursionError in SSL. Either keep preload off, or patch at the very top of gunicorn.conf.py:
from gevent import monkey
monkey.patch_all()
3. Database connections
Each greenlet that touches the ORM gets its own Django database connection (connections are per thread or greenlet). With worker_connections = 100 and two workers, up to 200 PostgreSQL connections can open under load, beyond many servers' max_connections. Mitigations:
- Put PgBouncer (transaction mode) in front of PostgreSQL.
- Keep
CONN_MAX_AGElow (0 or a few seconds) so idle greenlets do not hold connections. - Lower
worker_connectionsto what the database can serve.
For psycopg2, use psycogreen (psycogreen.gevent.patch_psycopg()) so database waits yield too; psycopg 3 should be checked against your version's gevent support, or use gthread if in doubt.
4. Avoid CPU-bound and blocking C code
gevent only switches on patched I/O. A request that renders a huge template, resizes an image, or calls a C library that blocks without releasing control stalls all greenlets in that worker. Move such work to Celery, or run those endpoints in a separate service with sync workers.
5. Measure under realistic load
Load-test an endpoint that calls a slow dependency (for example a mock API with 500 ms latency) with gthread and gevent configurations at the same memory budget. gevent should sustain much higher concurrency without queueing; if not, something is blocking.
Verification check: under a load test with slow upstream calls, requests per second rise and p95 latency falls compared with sync workers; PostgreSQL's pg_stat_activity connection count stays within limits; no RecursionError or SSL hangs appear in logs.
Explanation
gevent implements lightweight cooperative threads (greenlets) on an event loop. Monkey patching replaces blocking standard-library I/O with versions that yield to the loop while waiting, so ordinary synchronous Django code effectively becomes concurrent during network waits. This works without touching view code, but only for patched I/O: unpatched C extensions and CPU-bound code still block the whole worker. Because Django allocates database connections per greenlet, concurrency translates directly into database connections, which is the most common production surprise.
Edge cases / notes
- Celery also supports a gevent pool for I/O-bound tasks, with the same caveats.
- Channels and WebSockets need ASGI, not gevent.
- Debugging blocking calls: gevent's
monitor_threadcan report greenlets that block the loop too long. - Python version support for gevent follows a release lag; check compatibility before upgrading Python.
Next steps
- Compare worker strategies in How to Optimize Gunicorn Workers for Django.
- The async alternative: When Async Django Views Pay Off in Production.
FAQ
When should I use gevent workers with Django?
When many requests spend most of their time waiting on external network calls and you do not want to rewrite views as async.
Do gevent workers need code changes?
Usually not for views, but database drivers and libraries must cooperate with monkey patching.
Why did PostgreSQL run out of connections after switching to gevent?
Each concurrent greenlet can hold a database connection. Pool with PgBouncer and lower worker_connections.
Is gevent faster than threads?
For many concurrent slow I/O calls, it supports higher concurrency with less memory. For CPU-bound work, neither helps.