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

Uploaded CPython 3.14musllinux: musl 1.2+ ARM64

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

Uploaded CPython 3.14manylinux: glibc 2.28+ ARM64

zuvloop-0.0.4-cp314-cp314-macosx_11_0_arm64.whl (330.6 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

zuvloop-0.0.4-cp314-cp314-macosx_10_15_x86_64.whl (331.4 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

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

File metadata

  • Download URL: zuvloop-0.0.4.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.4.tar.gz
Algorithm Hash digest
SHA256 3c98f24486324ed407456bae266bf025f9bc91a58c1dfff530c94c663464513a
MD5 125b961cf96b6de21c142a891b77460e
BLAKE2b-256 1c02acfc6cf12934ee27c5c4b5fef121b4ac037297198edf786e516d5e087f8c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.4-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 704aba7bdb3364662b4763a479a4e0d41955830f2f01bd48495f36aa24f802de
MD5 5768aa861d385ec6617f0ea7850714a4
BLAKE2b-256 0566c717f926f0d8f855bf6c15dd0df5fa714b13321517910323cd9622852fc8

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.4-cp314-cp314-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 5574d0652f5547c091d7fe585ae9f5f85b29cabdd6093c714bff570a702c2148
MD5 5ec0e9793e2b36aa31cb0150b7b88246
BLAKE2b-256 efde9465ed55c08fced11aa4c7291ed16c20ff0161dc0fdbb709bb8853ddc278

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.4-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 7e04958f2ed7f809ce27afd923fcf1e61fe79d366102d74beb0e69ca0d1d5cdc
MD5 c83e4a46299dd406380307e4559dc908
BLAKE2b-256 ef1fc3e7883c037faa87d21a694ec89ef8af2971c5b575fb0672ba71624cd147

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.4-cp314-cp314-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 5f86ef2b2e3a3352eadd06f80bf2e58fbdc043d2493a24c19f7d8607cc680ec9
MD5 e716455d4f06ee9477ee64a1858747da
BLAKE2b-256 bcae1fd54c1e72b3c2f0d40727f546fcec54523be542de120a2ab7142040f533

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.4-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e4ef1251205235a7b247dd7c610cf63ce2bebe54c8e8a6b8fef3fc8361c5512c
MD5 7e27c92750e43d392b28aaedd03b6752
BLAKE2b-256 7fce96a8401cbfa914ea56eb3be21418ea4b24e1e347fb3185b74b534d176e79

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.4-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 e3655e04bda3ad89410ebfe7c5c2a2fc72cf220cc83e04ac587b84a928c36091
MD5 03f8fbd3dd71b61c2392000af4cdfd7b
BLAKE2b-256 bcd7e0db7fa26b5b1fc758bab9ca1c54ea71e3a542a072cc3940c066849f0cc4

See more details on using hashes here.

Provenance

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