gevent vs asyncio for I/O-Bound Services¶
gevent and asyncio solve the same problem — thousands of concurrent I/O waits in one thread — in opposite ways. gevent monkey-patches the standard library so ordinary blocking code (socket, requests, time.sleep) yields to an event loop implicitly; asyncio makes every yield point explicit with await and needs libraries written for it. On a benchmark of 2,000 GET requests at a concurrency of 100 against a local server that waits 10 ms per request, gevent with requests reached 2,624 requests per second, asyncio with aiohttp 8,670, and both peaked around 45–46 MiB of resident memory. The library mattered as much as the model: asyncio with httpx managed only 236 at that concurrency in the same test. This guide explains where each model fits, what monkey-patching costs, and how to move from one to the other.
Prerequisites¶
- Python 3.11+; the benchmark uses
pip install gevent requests aiohttp httpx. - The model comparison, from Threading vs Multiprocessing vs Asyncio.
- Client choice, from choosing between httpx and aiohttp.
1. See the two programming models side by side¶
gevent code is ordinary blocking code with a patch at the top of the entry point:
from gevent import monkey; monkey.patch_all() # must run before other imports
import gevent.pool
import requests
session = requests.Session()
session.mount("http://", requests.adapters.HTTPAdapter(pool_maxsize=100))
def fetch(url: str) -> int:
return session.get(url).status_code # blocks... the greenlet, not the process
pool = gevent.pool.Pool(100)
codes = pool.map(fetch, URLS)
The asyncio equivalent marks every wait:
import asyncio
import aiohttp
async def main(urls):
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(limit=100)) as s:
sem = asyncio.Semaphore(100)
async def fetch(url):
async with sem, s.get(url) as r:
await r.read()
return r.status
return await asyncio.gather(*(fetch(u) for u in urls))
The gevent version runs existing synchronous libraries concurrently with no code changes; the asyncio version requires async libraries but makes every suspension point visible in the source. That visibility is asyncio's main safety property: code between two awaits cannot be interleaved with other tasks, which makes reasoning about shared state much easier than in gevent, where any function call might switch greenlets.
Verify: both versions return the same results; only the gevent one runs unmodified sync code.
2. Understand what monkey-patching changes¶
monkey.patch_all() replaces socket, ssl, select, threading, time.sleep, subprocess and more with cooperative versions. Three consequences follow:
- It must run first. Modules imported before the patch keep references to the original blocking functions. A library that did
from socket import socketbefore patching blocks the whole process. - C extensions are not patched. A database driver that does I/O in C (
psycopg2without its gevent wait callback,mysqlclient) blocks every greenlet. gevent-aware drivers or pure-Python drivers are required. - Threads become greenlets.
threading.Threadis patched to spawn greenlets, so code that expected real parallelism — or real blocking in a thread — behaves differently.
asyncio has the inverse problem: nothing is patched, so a blocking call in a coroutine visibly blocks the loop, and the debugging tools in finding blocking calls with asyncio debug mode find it. In gevent, an unpatched blocking call is equally fatal and much harder to spot, because nothing in the code marks where switching is supposed to happen.
Verify: in a gevent service, run with gevent.config.monitor_thread = True (the hub monitor) to report greenlets that block the hub for too long.
3. Compare operational behaviour, not just throughput¶
The benchmark measured one thing; production cares about several:
| Concern | gevent | asyncio |
|---|---|---|
| existing sync code | runs as is after patching | must be rewritten or wrapped |
| suspension points | implicit, anywhere | explicit await |
| blocking C extension | stalls everything, silently | stalls the loop, visible in debug mode |
| cancellation and timeouts | gevent.Timeout, greenlet kill |
structured: TaskGroup, asyncio.timeout |
| ecosystem direction | maintenance mode in many frameworks | where new libraries are built |
| interop | conflicts with asyncio in one process | native in modern frameworks |
The last two rows usually decide new projects: async-first libraries — database drivers, SDKs, ASGI frameworks — target asyncio, and running gevent and asyncio in one process is fragile. Structured concurrency, covered in structured concurrency with asyncio.TaskGroup, has no direct gevent equivalent.
Verify: for an existing gevent service, list its C-extension dependencies and confirm each is gevent-safe.
4. Know when gevent is still the right answer¶
gevent remains a good choice in specific situations:
- A large synchronous codebase — a Flask or Django app with sync views and sync libraries — that needs more concurrency on I/O, today, without a rewrite.
gunicorn -k geventis the shortest path from "one request per thread" to "thousands per process". - Libraries with no async equivalent, all pure Python or gevent-aware.
- Short-lived scripts that fan out a few thousand requests using a sync client you already depend on.
It is the wrong choice for new services that will be maintained for years, for code that must use async-only libraries, and for anything mixing in asyncio. If a gevent service needs one async-only dependency, the bridge in running an event loop in a background thread is possible but delicate under monkey-patching, because the "thread" may be a greenlet.
Verify: for each service, the choice is recorded with the reason — compatibility or explicitness — and revisited when dependencies change.
5. Migrate from gevent to asyncio incrementally¶
Rewriting a gevent service in one step is rarely feasible. A workable path:
- Measure where time goes; the I/O-heavy endpoints are the ones that benefit.
- Move the edge to ASGI with sync views still running (Django ASGI, or a Flask app behind an ASGI adapter), so new endpoints can be async.
- Port hot paths to async libraries one at a time, keeping sync code in threads via
to_thread. - Remove
monkey.patch_all()only when nothing depends on it; test with it disabled long before deleting it.
# transitional async endpoint calling remaining sync code safely
async def report(request):
rows = await asyncio.to_thread(legacy_sync_report, request.args) # old code, real thread
async with aiohttp.ClientSession() as s: # new code, async
await s.post(WEBHOOK, json={"rows": len(rows)})
return JSONResponse(rows)
The general strategy, including running old and new paths side by side, is in migrating legacy threading code to asyncio without downtime.
Verify: each migrated endpoint is benchmarked against its gevent version before the old path is removed.
Verification¶
The choice is sound when:
- Throughput is measured with the real client libraries, since they can dominate the result.
- For gevent, every C extension is gevent-safe and the hub monitor reports no long blocks.
- For asyncio, debug mode finds no blocking calls in request paths.
- Mixing models in one process is avoided, or isolated behind a tested bridge.
Diagnostic Hook: in gevent services, enable the hub monitor and alert on its "greenlet blocked the hub" reports; in asyncio services, alert on event loop lag. Both measure the same failure — one unit of work stalling everyone else — and a service migrating from one model to the other should keep both until the migration is complete.
Pitfalls & edge cases¶
- Patching after imports. Modules that grabbed blocking functions first stay blocking.
- C-extension database drivers under gevent. They block every greenlet unless explicitly gevent-aware.
- Benchmarking the model and not the library. Client implementations varied by more than 30x in the same test.
- Running gevent and asyncio together. Possible with care, fragile in practice; pick one per process.
Frequently Asked Questions¶
Is gevent faster than asyncio?
It depends mostly on the libraries. In testing, gevent with requests reached 2,624 requests per second, asyncio with aiohttp 8,670, and asyncio with httpx 236, at the same concurrency against the same server. Memory use was similar.
What does gevent monkey patching do?
It replaces blocking standard library functions such as socket, ssl, select and time.sleep with cooperative versions, so ordinary synchronous code yields to gevent's event loop while it waits.
Should I use gevent for a new project?
Usually not. New async libraries target asyncio, structured concurrency has no gevent equivalent, and implicit switching makes shared-state bugs harder to see. gevent fits large existing sync codebases that need I/O concurrency quickly.
Can I use asyncio and gevent in the same process?
It is possible with bridging libraries but fragile, since monkey patching changes threading and sockets underneath asyncio. Prefer one model per process.
Related¶
- Threading vs Multiprocessing vs Asyncio — up to the topic overview.
- asyncio vs threading for 1000 concurrent HTTP requests — the thread-pool side of the same comparison.
- Concurrent Execution & Worker Patterns — the section overview.