Open-source project
Kludex/zuvloop avatar
Kludex/zuvloop

zuvloop: a libuv asyncio loop written in Zig, with the limits that come with it

A libuv event loop for asyncio, written in Zig.

67 stars4 forksPythonMIT

At a glance

What is it?
zuvloop swaps the standard asyncio event loop for a libuv loop compiled from Zig, installs with one pip command, and reports large scheduling gains alongside a weaker aiohttp client result. It is alpha software for Python 3.14 and newer.
Who is it for?
Adopt zuvloop only on Python 3.14 or newer, on a platform with a prebuilt wheel, and start by running your own test suite with the loop factory before anything else. Stay on stock asyncio or uvloop if you need a stable release, Windows support from a mature project, or a loop that has been exercised outside benchmarks.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The scheduling overhead zuvloop is aimed at

The default asyncio loop is written in Python, and the cost of a callback shows up in the places that run hottest: call_soon, timer bookkeeping, and thread-safe handoffs. The README publishes a benchmark table from benchmarks/ measured on an M3 Max, macOS 26, CPython 3.14 and libuv 1.51.0, where call_soon_threadsafe reaches 11.9M/s against asyncio's 0.44M/s and timer schedule plus cancel reaches 9.02M/s against 1.17M/s. Those are the numbers the project leads with, and they are the reason to look at it at all.

The audience is narrow but real. If you run an asyncio service on Linux or macOS, on a recent CPython, and you have already profiled the loop itself rather than your handlers, zuvloop is aimed at you. If your bottleneck is a database query or JSON serialisation, the loop is not your problem and swapping it will move nothing. The README does not claim otherwise, but the benchmark table makes it easy to forget.

What actually changes under the loop

zuvloop is a replacement event loop, not a framework and not a runtime. The README states that your code stays the same and the loop underneath changes, and that Task objects, protocols and APIs are standard asyncio. The architecture notes describe argument storage inside handles so no tuple is allocated 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. The last one shows up in the table as 1.70M/s for a numeric host against asyncio's 27.0k/s, which is the difference between parsing a string and resolving one.

The loop is driven by libuv, the same engine behind Node.js, and the native side is compiled from Zig. The extension is built with hatch-ziglang and a pinned ziglang==0.16.0 during the isolated wheel build, and libuv is compiled into the extension, which is why the wheel carries libuv's licence alongside the MIT one. Nothing here changes how coroutines are written. It changes how quickly the machinery around them moves.

Install zuvloop and run a first connection

The README gives a single install command. Prebuilt wheels exist for Linux x86-64 and AArch64, macOS x86-64 and arm64, and Windows AMD64 and ARM64, so a normal pip install should not compile anything.

bash
pip install zuvloop

Source distributions behave differently. According to the README, they install a pinned Zig 0.16 toolchain in their isolated build environment, and direct native development commands require Zig 0.16 on PATH. If you are on a platform without a wheel, expect a build step and a Zig dependency.

The first real use is the README's own example: ordinary asyncio code, with zuvloop.run replacing asyncio.run.

python
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())

You should see the first 64 bytes of the HTTP response printed. If you would rather not touch the entry point, the README offers the loop factory route, which keeps asyncio.run in place.

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

That is the whole migration as documented. There is no configuration file, no environment variable, and no zuvloop-specific setup for the observability path either.

Where zuvloop is slower, and where the documentation stops

The benchmark table is honest enough to include a loss. On aiohttp client, zuvloop measures 14.8k req/s against uvloop's 15.0k req/s. It is a small gap and the README frames the overall result as matching or beating uvloop on 13 of 14 benchmarks, which is a fair reading. It is still the row to check against your own workload, because client-side HTTP is a common shape.

The larger limitation is maturity. pyproject.toml classifies the project as Development Status :: 3 - Alpha, and the version numbers are v0.0.x. The README does not document a rollback path, a deprecation policy, or what happens to in-flight tasks if the native layer misbehaves. There is no upgrade guide in the README. Compatibility is enforced by three aggregate gates in CI, including CPython conformance, pinned upstream suites from aiohttp, uvicorn, AnyIO, websockets, aioquic, Tornado and HTTPX2, a native sanitizer build and a 500-cycle resource-ownership soak, and the README points at .github/workflows/compatibility.yml as the source of truth. That is a serious test posture for a 0.0.x project. It is not the same as production mileage, and the README does not claim it is.

zuvloop against uvloop and stock asyncio

uvloop is the obvious comparison and the README makes it explicitly, benchmarking against it and describing zuvloop as matching or beating it on 13 of 14 rows. The approaches differ in the implementation language and in what ships with the loop. uvloop is the older, more widely deployed libuv loop. zuvloop is a newer libuv loop with the native side in Zig, and it bundles OpenTelemetry instrumentation as part of the runtime surface: zuvloop.slow_callback spans with start and end timestamps measured by uv_hrtime() in native code, zuvloop.unhandled_exception spans, counters, a callback-duration histogram and loop gauges such as loop_count, events, idle_time_ns, ready, timers and watchers.

The dependency footprint of that instrumentation is deliberately small. pyproject.toml lists opentelemetry-api>=1.23 and typing-extensions>=4.15 as the only runtime dependencies, with no SDK. Providers can be configured before the loop starts or from inside it, and the README notes that logfire.configure() inside main() works because zuvloop checks at each run_forever() entry and re-checks on loop.metrics_interval, 10 seconds by default. Until a provider is installed the instruments are no-ops and slow-callback timing stays off. That is a real design difference from uvloop: observability is part of the loop rather than something you bolt on around it.

Against stock asyncio the difference is simpler. asyncio is bundled with CPython, requires no wheel, and will never fail to install. zuvloop is an extra dependency with a native extension and a Python 3.14 floor.

Maintenance cost and what the licence means for a wheel

The last push to the repository was on 2026-08-28, the same day v0.0.12 was released, and the repository is not archived. Three releases landed within two days of each other in late August 2026, which suggests active work rather than a frozen snapshot, but the version numbers and the alpha classifier mean you should budget for reading release notes before upgrading.

The build system is pinned deliberately. pyproject.toml comments that hatch-ziglang runs during the isolated wheel build and invokes the native compiler, so both hatch-ziglang==0.2.1 and ziglang==0.16.0 are pinned exactly, with the note that a future version resolved from a lower bound could execute unreviewed code into the wheels the publish job trusts. If you build from source, that pin is load-bearing for your own supply chain, not just the project's.

On licensing: the project is MIT, and license-files includes both LICENSE and vendor/libuv/LICENSE*, because libuv is compiled into the extension and its licence ships with the wheel. If your organisation scans wheel contents for third-party licences, expect two entries, not one. This is a description of what the files say, not legal advice.

Editorial conclusion

Adopt zuvloop only on Python 3.14 or newer, on a platform with a prebuilt wheel, and start by running your own test suite with the loop factory before anything else. Stay on stock asyncio or uvloop if you need a stable release, Windows support from a mature project, or a loop that has been exercised outside benchmarks. Verify two things first: that pip resolves a wheel for your interpreter and platform, and that the aiohttp client path, where zuvloop measured 14.8k req/s against uvloop's 15.0k req/s, is not on your critical path.

Frequently asked questions

How do I use async event loops in Python with zuvloop?

Write normal asyncio code and either call zuvloop.run(main()) instead of asyncio.run(main()), or pass zuvloop.new_event_loop as the loop_factory to asyncio.run. The README describes this as the whole migration, with no configuration file or environment variable involved.

How does the event loop work in zuvloop?

It is a libuv-backed loop with the native side written in Zig. The architecture notes describe argument storage inside handles to avoid a 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.

Is asyncio built into Python, or do I need zuvloop?

asyncio ships with CPython, so zuvloop is an optional replacement rather than a requirement. The README presents it as a drop-in loop that keeps standard Task objects, protocols and APIs, and it requires Python 3.14 or newer.

Official sources

  1. Official README
  2. Project repository
  3. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/kludex-zuvloop.svg)](https://hysenlabs.com/projects/kludex-zuvloop)