Skip to content

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

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,000 GETs at concurrency 100 against a 10 ms server 3 horizontal bars comparing asyncio + aiohttp with the others. 2,000 GETs at concurrency 100 against a 10 ms server asyncio + aiohttp 8,670 req/s, 46 MiB gevent + requests 2,624 req/s, 45 MiB asyncio + httpx 236 req/s, 56 MiB Python 3.14, gevent 26.9, aiohttp 3.14, httpx 0.28; server: aiohttp on the same host. Same model, different library: the HTTP client mattered as much as gevent versus asyncio.

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 socket before patching blocks the whole process.
  • C extensions are not patched. A database driver that does I/O in C (psycopg2 without its gevent wait callback, mysqlclient) blocks every greenlet. gevent-aware drivers or pure-Python drivers are required.
  • Threads become greenlets. threading.Thread is 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.

gevent versus asyncio beyond throughput A grid of 5 rows by 3 columns. gevent versus asyncio beyond throughput concern gevent asyncio existing sync code runs after patch_all needs async libraries where switches happen anywhere, implicitly only at await blocking C extension stalls all greenlets stalls loop, detectable cancellation Timeout, kill TaskGroup, asyncio.timeout new libraries target rarely usually gevent buys compatibility with sync code; asyncio buys explicitness and the modern ecosystem.

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 gevent is 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:

  1. Measure where time goes; the I/O-heavy endpoints are the ones that benefit.
  2. 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.
  3. Port hot paths to async libraries one at a time, keeping sync code in threads via to_thread.
  4. 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.

gevent or asyncio for this service? A decision on What is the starting point with 3 outcomes. gevent or asyncio for this service? What is the starting point? large sync codebase, need concurrency now gevent gunicorn -k gevent new service or async-only deps asyncio explicit awaits long-lived gevent service migrate gradually edge to ASGI first gevent is a compatibility tool; asyncio is where new I/O-heavy Python code lives.

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.