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. Over 20x faster than asyncio at thread-safe scheduling and faster than uvloop on 10 of the 11 benchmarks 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.3, and libuv 1.51.0. The labels show the absolute numbers.

Benchmark asyncio uvloop zuvloop
call_soon 2.35M/s 5.05M/s 6.54M/s
call_soon with arguments 2.27M/s 3.53M/s 6.07M/s
call_soon_threadsafe 0.38M/s 5.15M/s 8.48M/s
timer schedule + cancel 1.46M/s 2.37M/s 8.95M/s
bulk stream 6.6 GiB/s 6.9 GiB/s 9.4 GiB/s
echo round trips, 1 KiB 37.2k/s 42.3k/s 51.9k/s
uvicorn, plaintext 47.0k req/s 63.6k req/s 67.3k req/s
uvicorn, 10 KiB body 44.5k req/s 59.9k req/s 64.3k req/s
aiohttp server 42.5k req/s 51.7k req/s 52.9k req/s
aiohttp client 9.49k req/s 11.5k req/s 11.1k req/s
getaddrinfo, numeric host 20.7k/s 1.47M/s 1.77M/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

Source distributions install a pinned Zig 0.16 toolchain in their isolated build environment. Direct native development commands require Zig 0.16 on PATH. Free-threaded CPython builds are not supported and fail explicitly at build time.

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. The in-repository suite and CPython-derived conformance run on every change. A weekly workflow tests CPython 3.14.0, the newest 3.14 patch, the 3.15 prerelease, and immutable uvicorn 0.52.3 and aiohttp 3.14.3 commits.

The following counts are the recorded compatibility baseline that motivated those automated gates; the workflow, not this table, is the current source of truth:

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 the hatch-ziglang build hook 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.6.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.6-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.6-cp314-cp314-musllinux_1_2_aarch64.whl (1.5 MB view details)

Uploaded CPython 3.14musllinux: musl 1.2+ ARM64

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

Uploaded CPython 3.14manylinux: glibc 2.28+ ARM64

zuvloop-0.0.6-cp314-cp314-macosx_11_0_arm64.whl (334.2 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

zuvloop-0.0.6-cp314-cp314-macosx_10_15_x86_64.whl (335.1 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

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

File metadata

  • Download URL: zuvloop-0.0.6.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.6.tar.gz
Algorithm Hash digest
SHA256 856ee2d9aac7eb6f75fa05b548fbb5e000728d82a01f7ed8e574d62af5f65aca
MD5 3fefc89260005ebfada387c97b930967
BLAKE2b-256 a59a5a7fc81717cdcaee9801af6bed11a7e95dc76dd24289bfe3c771f36cb0b5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.6-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 c0017412d76007f57a3773239342ba6721c25c88361d4a825bf0f729f028e55a
MD5 76eb99a049696588675c824062e36f6f
BLAKE2b-256 348bb850589f9b945ed92836cbec990a76b6b8ea66440d37369a1a556bb466db

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.6-cp314-cp314-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 36eacd6a789f7f50e29a246af5df17f81519062211373efe011fcd2e6899439c
MD5 b8131397d68feeae21ea4506fe38c518
BLAKE2b-256 5fde7839ffc3e231a207e8fbdd09c361b7ddb17d2e9ea2b73a4d6726d15c37c4

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.6-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 c09d9c86fd9233d2ad6a900301d43c2e2d5a9aceee5863b06b873e498fcf00fa
MD5 e1fe1a200835646b256361f1f6102c89
BLAKE2b-256 0babcc1336fe13318b093a9493fae9ed283acaef58fc9f6c5535ab7c920318b5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.6-cp314-cp314-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 67758f0451c791c91e10f785e73fc212c8982b6228e3f3749c092d1c65c46c37
MD5 fe14f75145559099d96bbb318f547525
BLAKE2b-256 aedd2ea5dcb1259c95664b75a2b85e24f3a03468458ab9df59d72ef6d5301560

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.6-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c2003ade22e937451cff4a43011be73edd2ea97a838df4edc5a113281d69a58f
MD5 8d6f84f4b477bf29fbd3e5bda35067d5
BLAKE2b-256 6ceda84d4690ac6d97ff6ad47ea2914afff562c53ab6094d3ed3669b7b4f8928

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.6-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 b0f52cf1f68bbfea6dd636d4e5336661b7eed701f8f5c752e67a228e8ac62215
MD5 14bb2c3307475328e4f3400c610cdba8
BLAKE2b-256 b2770c75eca93525c0bb1236909e5a5d0afeb4d40446eb287a3d829efa7dd55c

See more details on using hashes here.

Provenance

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