gevent
gevent makes ordinary blocking Python code run concurrently without async and await. It runs your functions inside greenlets, which are lightweight coroutines from the greenlet library, and drives them with a libev or libuv event loop. The trick is monkey patching: gevent.monkey.patch_all() swaps the standard library's socket, ssl, select, threading, time and subprocess modules for cooperative versions, so a plain requests.get or a psycopg2 query yields to the event loop while it waits on the network instead of blocking the process. Code you already wrote keeps its shape; a call that used to hold up one worker now lets thousands of other greenlets run. It also ships stdlib-shaped concurrency tools of its own: gevent.pool, gevent.queue, gevent.event, gevent.lock and a WSGI server.
Still the most practical way to get a large blocking Python codebase serving thousands of connections without rewriting it, and the gunicorn gevent worker has kept plenty of services alive that would otherwise need a port to asyncio. For new code the implicit switching, the C-extension audits and the lack of a free-threading story make it a hard sell against asyncio.
Use it if
- You have an existing synchronous codebase built on requests, psycopg2 or a Django or Flask app and need thousands of concurrent connections without rewriting every function as a coroutine
- You deploy WSGI behind gunicorn, where the gevent worker class is the standard way to serve an I/O-bound app at high concurrency with a small number of processes
- Your bottleneck is waiting on sockets rather than burning CPU, for example an API gateway, a crawler, or a service that fans out to a dozen internal endpoints per request
- You want concurrency primitives that look like the ones you already know, since gevent.queue.Queue, gevent.event.Event and gevent.lock.Semaphore mirror the queue and threading APIs closely enough to port code mechanically
- You need per-greenlet timeouts and cancellation, which gevent.Timeout applies to any blocking call in the block without the callee knowing anything about it
- You are starting something new. asyncio is in the standard library, its await points are visible in the source, and the library ecosystem has moved there. gevent is a way to retrofit concurrency onto blocking code, not a design you would pick from scratch in 2026
- You depend on C extensions that do their own network I/O. Patching the socket module does nothing for a driver that calls into libpq or libmysqlclient directly, so psycopg2 needs psycogreen to cooperate and mysqlclient never will. Every native dependency has to be audited individually
- Your roadmap includes free-threaded CPython. gevent explicitly does not support the no-GIL build; it compiles there but forces the GIL back on, and the greenlet library it depends on carries resource-leak caveats in that mode
- You are deploying on Windows. The project calls it a tier 2 best-effort platform and says outright it is not recommended for production, and with the libev backend it is capped near 1024 open sockets
- You need debuggability. Switching is implicit, so a stack trace shows where a greenlet was but not why it yielded, ordinary profilers and debuggers get confused by greenlet stacks, and one CPU-bound greenlet quietly stalls every other greenlet in the process
- You are pinned to Python 3.9 or PyPy. 26.4.0 removed 3.9 support and wheels now start at cp310 even though the package metadata still advertises requires_python >=3.9, and 26.5.0 announced that PyPy and 3.10 support are both ending soon
Setup reality
pip install gevent is usually a wheel download: 26.7.0 ships 45 of them covering CPython 3.10 through 3.15 on manylinux_2_28, musllinux, macOS and Windows including ARM64. It pulls greenlet, zope.event and zope.interface, plus cffi on Windows. On Python 3.9 the metadata claims support but no wheel exists, so pip falls back to compiling libev from source and you need a C toolchain. The install is the easy part. Monkey patching has to happen at the very top of your entry module, before anything imports socket, ssl or threading, or you get a MonkeyPatchWarning at best and half-patched modules that block the loop at worst; python -m gevent.monkey yourscript.py does the patching from outside when you cannot control import order. After that, audit every native dependency for blocking calls, install psycogreen if you use psycopg2, and be aware that GEVENT_LOOP picks between libev and libuv, with the Cython libev backend as the default on CPython outside Windows.
Patterns
Patch the standard library before anything elsemonkey-patch-first
# main.py: these two lines must come before every other import
from gevent import monkey
monkey.patch_all()
import requests
import gevent
urls = ['https://example.com', 'https://example.org', 'https://example.net']
jobs = [gevent.spawn(requests.get, u) for u in urls]
gevent.joinall(jobs, timeout=10)
print([j.value.status_code for j in jobs if j.value])If any module imported ssl, socket or threading before patch_all runs, gevent prints a MonkeyPatchWarning and those references stay blocking. When you cannot control import order, for example under a framework runner, use python -m gevent.monkey yourscript.py to patch from outside.
Run work concurrently and gather resultsspawn-and-collect
import gevent
def fetch(item):
return expensive_io(item)
jobs = [gevent.spawn(fetch, i) for i in items]
gevent.joinall(jobs)
results = []
for job in jobs:
if job.successful():
results.append(job.value)
else:
log.warning('failed: %r', job.exception)An exception inside a greenlet does not propagate to the spawner; it is stored on job.exception and the default hub prints it to stderr. Checking successful() on every job is the difference between handling failures and silently dropping them.
Cap how many greenlets run at oncebound-concurrency
from gevent.pool import Pool
pool = Pool(50)
for url in one_million_urls():
pool.spawn(fetch, url)
pool.join()
# map-style, returns results in order
results = Pool(50).map(fetch, urls)Greenlets are cheap but file descriptors and the remote server are not. Pool.spawn blocks once the pool is full, which applies backpressure to your producer loop; an unbounded gevent.spawn over a million URLs will exhaust descriptors instead.
Put a deadline on any blocking codetimeouts
import gevent
from gevent import Timeout
with Timeout(5, False) as t:
data = legacy_client.read_everything() # knows nothing about gevent
# on expiry the block exits and data is unbound
# raise instead of returning quietly
try:
with Timeout(5):
data = legacy_client.read_everything()
except Timeout:
data = None
# one-shot form
result = gevent.with_timeout(5, legacy_client.read_everything, timeout_value=None)Timeout works by raising inside the greenlet at its next switch, so it only fires on code that yields. A tight CPU loop with no I/O will run past the deadline untouched, which is the same reason one busy greenlet stalls the whole hub.
Hand work between greenlets with a queueproducer-consumer
import gevent
from gevent.queue import Queue, JoinableQueue
q = JoinableQueue(maxsize=100)
def worker():
while True:
item = q.get()
try:
handle(item)
finally:
q.task_done()
workers = [gevent.spawn(worker) for _ in range(10)]
for item in source():
q.put(item)
q.join()
gevent.killall(workers)gevent.queue mirrors the stdlib queue API but blocks cooperatively instead of holding the thread. maxsize is what gives you backpressure; without it a fast producer builds an unbounded in-memory list. Remember to kill the workers, since they loop forever.
Wait for a result or a one-shot eventsignal-between-greenlets
from gevent.event import Event, AsyncResult
ready = Event()
def waiter():
ready.wait(timeout=30)
print('go')
result = AsyncResult()
def producer():
try:
result.set(compute())
except Exception as exc:
result.set_exception(exc)
value = result.get(timeout=10) # re-raises whatever set_exception gotEvent is a latch for many waiters with no payload; AsyncResult carries a single value or exception to whoever calls get(). AsyncResult.get re-raises in the calling greenlet, which is the cleanest way to move an error across a greenlet boundary.
Serve a WSGI app with the gevent workergunicorn-worker
# gunicorn.conf.py
worker_class = 'gevent'
workers = 4 # about one per CPU core
worker_connections = 1000 # greenlets per worker
def post_fork(server, worker):
from psycogreen.gevent import patch_psycopg
patch_psycopg()
# gunicorn -c gunicorn.conf.py app:applicationgunicorn monkey patches for you inside the gevent worker, so do not call patch_all yourself in the app module. Keep workers near the core count and scale concurrency with worker_connections; the psycopg2 patch has to run after fork, in each worker.
Run gevent's own HTTP serverstandalone-wsgi-server
from gevent import monkey; monkey.patch_all()
from gevent.pywsgi import WSGIServer
from gevent.pool import Pool
def app(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return [b'ok']
server = WSGIServer(('0.0.0.0', 8080), app, spawn=Pool(1000))
server.serve_forever()Passing spawn=Pool(n) caps concurrent requests; the default spawns an unbounded greenlet per connection, which a slowloris-style client will happily exploit. pywsgi is fine behind a reverse proxy but has no process management, so most deployments still put gunicorn in front.
Push uncooperative C code onto real threadsoffload-blocking-calls
import gevent
from gevent.threadpool import ThreadPool
cpu_pool = ThreadPool(4)
def handler():
# a C extension that ignores the patched socket module
result = cpu_pool.apply(legacy_native_call, (payload,))
return result
# or use the hub's shared pool
result = gevent.get_hub().threadpool.apply(legacy_native_call, (payload,))This is the escape hatch for drivers and libraries that never became cooperative. The work still contends for the GIL, so it only helps for calls that release it, which most well-behaved C extensions do around I/O. Size the pool deliberately rather than sharing the hub's.
Make psycopg2 yield instead of blockingcooperative-postgres
from gevent import monkey; monkey.patch_all()
from psycogreen.gevent import patch_psycopg
patch_psycopg()
import psycopg2
conn = psycopg2.connect(dsn) # now yields while waiting on the serverWithout psycogreen, every query blocks the entire hub because psycopg2 talks to libpq in C and never touches the patched socket module. Call patch_psycopg after patch_all and, under gunicorn, inside post_fork. psycopg 3 in async mode and asyncpg both sit outside this world entirely.
Find the greenlet that is stalling everythingdetect-blocked-loop
import gevent
gevent.config.monitor_thread = True
gevent.config.max_blocking_time = 0.5 # seconds
# or from the environment, before the process starts:
# GEVENT_MONITOR_THREAD_ENABLE=true GEVENT_MAX_BLOCKING_TIME=0.5 python main.py
from gevent.util import print_run_info
print_run_info() # tree of live greenlets and their stacksThe monitor runs on a real OS thread, so it still reports when the hub is wedged, printing a run-info dump for any greenlet that holds the loop past max_blocking_time. Set the config immediately after importing gevent, before patching. Memory monitoring needs psutil, which comes with the monitor extra.
Shell out without blocking the loopcooperative-subprocess
from gevent import monkey; monkey.patch_all()
import subprocess
from gevent.subprocess import Popen, PIPE
p = Popen(['ffmpeg', '-i', src, dst], stdout=PIPE, stderr=PIPE)
out, err = p.communicate(timeout=60)
print(p.returncode)patch_all replaces subprocess with gevent.subprocess, so plain subprocess.run also becomes cooperative. Reading a child's output with the unpatched module blocks every greenlet until the process exits, which is a common surprise in code that shells out inside a request handler.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| eventlet | PyPI | You need the same green-thread and monkey-patching model but are already invested in eventlet's API or an OpenStack-era codebase |
| anyio | PyPI | You are willing to write explicit async and await and want structured concurrency that runs on both asyncio and trio |
| uvloop | PyPI | You already use asyncio and just want a faster libuv-backed event loop under it, with no patching involved |
| trio | PyPI | You want explicit cancellation scopes and nurseries so that concurrency and failure paths are visible in the source |