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

Uploaded CPython 3.14musllinux: musl 1.2+ ARM64

zuvloop-0.0.3-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.3-cp314-cp314-manylinux_2_28_aarch64.whl (1.5 MB view details)

Uploaded CPython 3.14manylinux: glibc 2.28+ ARM64

zuvloop-0.0.3-cp314-cp314-macosx_11_0_arm64.whl (328.0 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

zuvloop-0.0.3-cp314-cp314-macosx_10_15_x86_64.whl (328.7 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

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

File metadata

  • Download URL: zuvloop-0.0.3.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.3.tar.gz
Algorithm Hash digest
SHA256 e392aa0a0f3aa8e2516dfe8ab970397413cb105bd1e806103d8da1fa6dcf03fc
MD5 e735ec6871194bb15640dc454eb6f541
BLAKE2b-256 ba1afa7ec1af6136ec54d916cb5ba9f258874000781eee0470637eecf41572b2

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.3-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 5e713339c6c9f553f77b077b402c87d48b820a64dc9db0e3486d1a0679acbb0b
MD5 28b9262a4660ca37820c388775a7b793
BLAKE2b-256 1b1d01c9b2a40cfbfdda79e2dd527e325975ef95d15c970ac4fbae6b8bea5efd

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.3-cp314-cp314-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 28dc0f7f9d745f0022bbacad45d448daed7d8d1113f71d8fa46e05c52406000f
MD5 4ac81a898ac46c0d54adb67875ce92f2
BLAKE2b-256 be7d9d77f6c1785264e38498006760291336660a13af1e190748e88c4b20da74

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.3-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 64f838c5e8330f92c5062ffea835335ecaa734060bd2a90c548f129a24079ab0
MD5 18a3d1aeb54107e4e0ae37ecfbb970b3
BLAKE2b-256 8aff0af827c6e6695fd4d0d8de9d784272510d28e386a003347c1f6b4ec97407

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.3-cp314-cp314-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 9362d290ef4e0aed7f158d6a925d0a10a4ec7aca559289cc2781e06a844e1006
MD5 5f1c637fc02c901fccac028696f17561
BLAKE2b-256 a333f57b7cd4f42fb24cd9c9b03a6c6092d8227c912a81e2acb36351468ef310

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.3-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 acaed18a0744108d00a6cf144a48949ed02fc0b2969f7acb9e19fe1e1ef48496
MD5 68c7c0874e07ab2a2e95e3f215bf50c5
BLAKE2b-256 c58d682a5b4694358981d224462a42b43fc5fbf6f0e068a6cc03851bd8690444

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.3-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 101a8cf391c8f6d6ad6dbcb66037a08687e5a7367935a9f12a3f40b30327124e
MD5 b9e49db133fe3ea25537121aaf260396
BLAKE2b-256 cb2f6f108051d1d158f38404f6a9c724fcf837c5e53483538ba2b335be57db9e

See more details on using hashes here.

Provenance

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