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. Providers can be configured before the loop starts or from inside it — logfire.configure() in main() works: zuvloop checks at each run_forever() entry and re-checks on its sampling interval (loop.metrics_interval, 10 seconds by default) while the loop runs. Until a provider is installed 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 (published only once a real provider is installed; without one the snapshot is dropped).

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.

One more deliberate divergence: 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 ruff format --check
$ ./scripts/check-zig  # requires ZLint 0.9.1 on PATH
$ 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> <sha256>.

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.5.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.5-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.5-cp314-cp314-musllinux_1_2_aarch64.whl (1.5 MB view details)

Uploaded CPython 3.14musllinux: musl 1.2+ ARM64

zuvloop-0.0.5-cp314-cp314-manylinux_2_28_x86_64.whl (1.6 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.28+ x86-64

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

Uploaded CPython 3.14manylinux: glibc 2.28+ ARM64

zuvloop-0.0.5-cp314-cp314-macosx_11_0_arm64.whl (331.6 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

zuvloop-0.0.5-cp314-cp314-macosx_10_15_x86_64.whl (332.8 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

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

File metadata

  • Download URL: zuvloop-0.0.5.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.5.tar.gz
Algorithm Hash digest
SHA256 379bc1d2ecea5cbe004d623262f1b586f65d922c5e14eefb26bb7e8ef666da03
MD5 5bca68c8a93d3795c17929bb2bde03db
BLAKE2b-256 8d2d5b73bef8e274b956a39fc5f89dea3696e595a821c9f2bb150e05bf4e5b6e

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5.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.5-cp314-cp314-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.5-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 ae962e9bd65204a3964b5ae8bd5028b738cdcefb2246a2407aab2e3d23d9f731
MD5 8c7f0c00bc6904c5a9051e19fc6475d9
BLAKE2b-256 dc09b5672cf40e71575a507b6f30cf6bd2b6c602107bbae7b747261c3352cc3b

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5-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.5-cp314-cp314-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.5-cp314-cp314-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 1bf3159990f5687b0b432a25d6a5738712c08811efb802a704724a63d371444c
MD5 b5c9f5b9d85821bc8b58b5812dd89421
BLAKE2b-256 8014a555d24e059457e727a0becd3dbf257622d9b54553cd5f63c32ecde3f545

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5-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.5-cp314-cp314-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.5-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 2d79e9ce3ed1caa2296a24a83cda0aac150470a7d4766aa926bdba2c09bc0dc8
MD5 f991a7c98581e443d629cf912e8a2a4c
BLAKE2b-256 28469ca80b79922f1609e30a8e24bcc811a71815e8e9ea91f1589005653ae225

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5-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.5-cp314-cp314-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.5-cp314-cp314-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 f6ce928c5f378ce68f392eb261c6674eac56af766f3fcde84e1eaf9de9c707cc
MD5 cef1cc4df05d891fbd7c9cf443dffc06
BLAKE2b-256 aa82dd9072e3e15316f681b57b6fb3814d84b7ce420eb836e604bd3b01dddbe9

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5-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.5-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.5-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 116337fdcae87d37b9a36c603f376517b81307b33cb8e29fd40a2f05e354986e
MD5 331e4b3c511d467286226fe00ce8a166
BLAKE2b-256 13824ca6d9812085f3f6da1928898a19a9ac77c7f3d85650b4595b7054ca039f

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5-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.5-cp314-cp314-macosx_10_15_x86_64.whl.

File metadata

File hashes

Hashes for zuvloop-0.0.5-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 44867060f7d2cf1521c806dce261e59ba7585846d42f06d71c7c108c3a05021f
MD5 7aec7b93ffb5a3406a9927aed9ef0a4a
BLAKE2b-256 451be44e930c1e63951c3e54ca31ea14104f99cdd5b50cbe27daf3f584cfe077

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.5-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