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.

gevent workersRequest waitson external APIgevent yieldsmonkey-patched I/OSame processserves other requestsDB connectionscan multiplygevent overlaps network waits in sync code, but each greenlet may hold a database connection.
  1. pip install gevent and run gunicorn config.wsgi -k gevent --worker-connections 100 --workers 2.
  2. Gunicorn monkey-patches the standard library when the gevent worker starts; make sure nothing imports ssl or socket users before that (avoid preload_app with gevent unless you patch first).
  3. 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).
  4. Cap database connections: concurrent greenlets times connections can exceed PostgreSQL's limit; use PgBouncer.
  5. Keep CPU-heavy work out: one CPU-bound request blocks every greenlet in that worker.
  6. Measure: compare throughput and latency under realistic slow-I/O load against gthread workers.

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()
sync vs gthread vs gevent vs ASGIConcurrency per workerCode changesMain risksync1noneslow calls block workersgthreadthreads (2-8)nonethread-safetygeventโœ“hundredsnone!DB connections, CPUblockinguvicorn (ASGI)โœ“hundreds!async viewsblocking calls in asyncgevent gives high concurrency to sync code, at the cost of hidden blocking risksdjango-deployment.com
For moderate I/O, gthread is often enough and simpler to reason about.

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_AGE low (0 or a few seconds) so idle greenlets do not hold connections.
  • Lower worker_connections to 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.

gevent safelyPatch before anything imports sslno preload, or patch in confDatabase driver cooperatespsycogreen or verified psycopg 3Connections capped or pooledPgBouncer, low CONN_MAX_AGECPU-heavy work moved to Celeryno blocking the loopLoad test vs gthreadconfirm the gainBlocking anywhere in a gevent worker stalls every request on itdjango-deployment.com
Watch PostgreSQL connection counts closely after switching.

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_thread can report greenlets that block the loop too long.
  • Python version support for gevent follows a release lag; check compatibility before upgrading Python.

Next steps

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.

Keep going