Skip to main content

zuvloop

A fast, drop-in asyncio event loop, powered by libuv and written in Zig ⚡


Documentation: https://zuvloop.marcelotryle.com

Source Code: https://github.com/Kludex/zuvloop


zuvloop is a replacement for the built-in asyncio event loop.

Your code stays the same. The loop underneath gets faster. 🚀

The key features are:

  • Fast: Scheduling, timers, sockets, and DNS run in native code, driven by libuv — the same engine behind Node.js. Up to 6x faster than asyncio and faster than uvloop on every benchmark below.
  • Drop-in: One line to switch. Everything is standard asyncio — same Task objects, same protocols, same APIs.
  • Fully typed: Ships type hints for everything and passes strict mypy. Your editor will love it. ✨
  • Observable: Built-in OpenTelemetry instrumentation — slow-callback spans, unhandled-exception spans, loop metrics. Zero cost until you turn it on.
  • Modern: Built for Python 3.14, including the new asyncio introspection tools (python -m asyncio ps, call graphs, and friends).

Performance

zuvloop vs uvloop vs asyncio benchmarks

Throughput relative to stock asyncio (higher is better), measured with the suite in benchmarks/ on an M3 Max, macOS 26, CPython 3.14. The labels show the absolute numbers.

Benchmark asyncio uvloop zuvloop
call_soon 2.59M/s 5.66M/s 5.90M/s
call_soon with arguments 2.45M/s 3.55M/s 6.36M/s
call_soon_threadsafe 0.44M/s 5.06M/s 6.51M/s
timer schedule + cancel 1.55M/s 2.57M/s 9.58M/s
bulk stream 8.6 GiB/s 9.0 GiB/s 10.7 GiB/s
echo round trips, 1 KiB 38.4k/s 53.0k/s 58.4k/s
uvicorn, plaintext 54.0k req/s 70.5k req/s 76.7k req/s
uvicorn, 10 KiB body 51.6k req/s 69.5k req/s 74.2k req/s
aiohttp server 48.8k req/s 61.0k req/s 62.6k req/s
aiohttp client 13.4k req/s 16.6k req/s 16.9k req/s
getaddrinfo, numeric host 28.3k/s 1.58M/s 1.91M/s

Curious how? The architecture docs explain the design: argument storage inside handles (no tuple per callback), a native timer heap behind a single uv_timer_t, per-turn vectored write batching, zero-copy reads, and a getaddrinfo fast path for address literals.

Requirements

  • Python 3.14+
  • Linux or macOS

Installation

$ pip install zuvloop

To build from source you also need Zig 0.16.

Example

Write normal asyncio code, run it with zuvloop:

import asyncio

import zuvloop


async def main() -> None:
    reader, writer = await asyncio.open_connection("example.com", 80)
    writer.write(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")
    await writer.drain()
    print(await reader.read(64))
    writer.close()
    await writer.wait_closed()


zuvloop.run(main())

Prefer to keep asyncio.run()? Hand it the loop factory:

asyncio.run(main(), loop_factory=zuvloop.new_event_loop)

That's it. That's the migration. 🎉

Observability

zuvloop emits plain OpenTelemetry. The only runtime dependency is opentelemetry-api — not the SDK, nothing vendor-specific. Configure providers before starting the loop; zuvloop checks for them at each run_forever() entry. Until then the instruments are no-ops and slow-callback timing stays off.

Anything that speaks OpenTelemetry can collect it. For example, with Logfire:

import logfire
import zuvloop

logfire.configure()  # installs the OTel providers


async def main() -> None: ...


zuvloop.run(main())

That's all — there is no zuvloop-specific setup. Spans and counters are emitted as events happen, and the loop gauges are sampled automatically while the loop runs (only when a real provider is installed, so an uninstrumented program never pays for sampling).

You get:

  • zuvloop.slow_callback spans — with real start/end timestamps measured by uv_hrtime() in native code, and the awaiting call graph attached (via asyncio.format_call_graph()), so you see why a callback was running, not just its repr.
  • zuvloop.unhandled_exception spans — with the exception recorded.
  • Counters, a callback-duration histogram, and live loop gauges (loop_count, events, idle_time_ns, ready, timers, watchers, ...).

And because zuvloop schedules real asyncio.Task objects, the Python 3.14 introspection tools work unchanged:

$ python -m asyncio ps <pid>
$ python -m asyncio pstree <pid>

Compatibility

zuvloop is checked against CPython's own conformance suite and against the test suites of the projects that exercise an event loop hardest — run unmodified, with the loop swapped underneath:

Suite Result
CPython test_asyncio 88 passed, 4 skipped, none failing
uvicorn 1257 passed, no failures
aiohttp 4473 passed, 36 failed — 33 of which also fail on stock asyncio

scripts/conformance.py runs CPython's EventLoopTestsMixin, SubprocessTestsMixin and BaseSockTestsMixin against zuvloop, downloading the source of whichever interpreter is running so the suite always matches it. Each test runs in its own process, so a hang is reported rather than stopping the run. Three of the four skips are white-box tests of CPython's own internals - two patch asyncio.base_events.socket, one counts calls to BaseEventLoop._run_once - which no loop outside the standard library can satisfy.

Of aiohttp's three remaining failures, two are blockbuster reporting a blocking os.stat that the standard library makes on the same path, and the third is the loop.time() difference below.

(For reference: uvloop cannot complete the aiohttp suite — it fails fifteen tests and then hangs.)

There is one intentional difference: patching loop.time() does not move the scheduler. zuvloop keeps its timer heap in native code and reads the clock directly, so monkeypatching time() — a trick some test suites use to fast-forward timeouts — changes what loop.time() returns and nothing else. A loop that needs a controllable clock should schedule against one explicitly.

Also not implemented: sendfile() and sock_sendfile() raise NotImplementedError. Handles returned by call_soon implement the asyncio.Handle interface but are not instances of it: the base class is 56 bytes of storage such a handle never writes, measured at 2% of call_soon, which is the object the loop allocates more often than any other. call_later and call_at do return real asyncio.TimerHandle instances, so they order and compare by deadline, and call_soon_threadsafe returns a real asyncio.Handle, because 3.14 requires cancelling one from another thread to block until a callback that has already started finishes.

Development

$ uv venv --python 3.14
$ uv pip install -e . --group dev
$ uv run pytest
$ uv run mypy
$ uv run ruff check
$ uv run --group bench python benchmarks/run.py

The extension is rebuilt by hatch_build.py on every install. To rebuild in place:

$ python scripts/build.py

vendor/libuv is an unmodified upstream release tarball; see vendor/README.md. Update it with ./vendor/update-libuv.sh <version>.

License

This project is licensed under the terms of the MIT license.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zuvloop-0.0.2.tar.gz (1.4 MB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

zuvloop-0.0.2-cp314-cp314-musllinux_1_2_x86_64.whl (1.5 MB view details)

Uploaded CPython 3.14musllinux: musl 1.2+ x86-64

zuvloop-0.0.2-cp314-cp314-musllinux_1_2_aarch64.whl (1.5 MB view details)

Uploaded CPython 3.14musllinux: musl 1.2+ ARM64

zuvloop-0.0.2-cp314-cp314-manylinux_2_28_x86_64.whl (1.5 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.28+ x86-64

zuvloop-0.0.2-cp314-cp314-manylinux_2_28_aarch64.whl (1.5 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.28+ ARM64

zuvloop-0.0.2-cp314-cp314-macosx_11_0_arm64.whl (324.6 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

zuvloop-0.0.2-cp314-cp314-macosx_10_15_x86_64.whl (325.5 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

Details for the file zuvloop-0.0.2.tar.gz.

File metadata

  • Download URL: zuvloop-0.0.2.tar.gz
  • Upload date:
  • Size: 1.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zuvloop-0.0.2.tar.gz
Algorithm Hash digest
SHA256 43e56ae66c2a84c0d83e0e3ef98a99b9c43a3bfbdc8dd9abf6cb44f854be6579
MD5 a8c42a9084a6986705e0ad7cc8396097
BLAKE2b-256 505e4a41c7f57382d279b7fe3aa1d9b5764c044dfe6555a178aad063b34f2d4f

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2.tar.gz:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zuvloop-0.0.2-cp314-cp314-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.2-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 1b949a3fbe1bdc8935b705e1528badbd34fab7a0f89197fdc74f41ab76aa82b9
MD5 48436bfa30781e6b2d5e7ea744fb6210
BLAKE2b-256 76bb0e367f531f42503b78701054f00109defe1004f037d5b471af4ac3eca255

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2-cp314-cp314-musllinux_1_2_x86_64.whl:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zuvloop-0.0.2-cp314-cp314-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.2-cp314-cp314-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 d2032b638e871ded379527da8793069dfd7526c904a46b6add87c2ac1477e93e
MD5 4187730f70603f5ab3445e530b6f6e7d
BLAKE2b-256 8bc6925d0fd957aa578ce1bda3dc604d883ba3b4d41d4edd5ae4b245376e5a2a

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2-cp314-cp314-musllinux_1_2_aarch64.whl:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zuvloop-0.0.2-cp314-cp314-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.2-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 95040d421268c3490ea2ffccfc05b8d7504794a1a983f7ba782290267aaa0f6e
MD5 fe693cc5b7753d5fc91508b7c6df2e2f
BLAKE2b-256 3e56b21d26a2f3493a7d192bda03a748a71995eb9d7092c5f78b090d9ecf38d0

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2-cp314-cp314-manylinux_2_28_x86_64.whl:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zuvloop-0.0.2-cp314-cp314-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.2-cp314-cp314-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 644697bca0e8b6e093f6137b69698e8ffa46ded21bb6111919dff02736b9aaee
MD5 c39140d90333a3637ecab0e4d3469f72
BLAKE2b-256 1cc31738d03c08592702d7f893be6670c48ada049b71fc24c83d6b966519019c

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2-cp314-cp314-manylinux_2_28_aarch64.whl:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zuvloop-0.0.2-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.2-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 8604192271c38fc3c5c44c7622f371d34793fbb29224ac079249df99ec8350a9
MD5 4cb3882d9a5c27d83a88558bd77ce67a
BLAKE2b-256 708be1f33b8f46bfbe16391d68a85ea944757099a8f676349eb939539e9dd7ec

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2-cp314-cp314-macosx_11_0_arm64.whl:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file zuvloop-0.0.2-cp314-cp314-macosx_10_15_x86_64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.2-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 985d695d3d7bcec3916df28db9344876ba342dcbd07302f12f31788568696b9c
MD5 784edee54d644b1f87da46ea5948dfa9
BLAKE2b-256 d2d8b505e3440f345368d01c5418768f2db8c11dba385f8a4dd47cb6600ef408

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.2-cp314-cp314-macosx_10_15_x86_64.whl:

Publisher: publish.yml on Kludex/zuvloop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page