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, macOS or Windows
  • Prebuilt wheels for Linux x86-64/AArch64, macOS x86-64/arm64 and Windows AMD64/ARM64

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. Its small runtime surface is opentelemetry-api plus typing-extensions for the shipped type declarations — no SDK and 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

Every pull request is held behind three stable aggregate gates. The first covers the in-repository suite on Linux and macOS, the portable suite on Windows, plus musl runtime tests, cross-compilation, ReleaseSafe builds and documentation. The second runs CPython conformance and pinned upstream suites from aiohttp, uvicorn, AnyIO, websockets, aioquic, Tornado and HTTPX2 with zuvloop swapped underneath. The third runs the native sanitizer build and a 500-cycle resource-ownership soak.

The weekly compatibility run also tests CPython 3.14.0, the newest 3.14 patch and the 3.15 prerelease, then exercises gRPC AsyncIO, asyncpg, Psycopg and redis-py against real local services. The immutable commits and exact commands in .github/workflows/compatibility.yml are the source of truth.

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.

The aiohttp compatibility run disables its optional blockbuster plugin because that plugin exempts stdlib asyncio calls by source filename and therefore reports equivalent os.stat and os.sendfile calls from any third-party loop. Its remaining strict expected failure is the loop.time() difference below. One concurrent WebSocket-close test is skipped because it assumes selector-loop ready/I/O ordering and fails intermittently on uvloop too; every other aiohttp test remains enforced.

(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.7.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.7-cp314-cp314-win_arm64.whl (417.7 kB view details)

Uploaded CPython 3.14Windows ARM64

zuvloop-0.0.7-cp314-cp314-win_amd64.whl (436.1 kB view details)

Uploaded CPython 3.14Windows x86-64

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

Uploaded CPython 3.14musllinux: musl 1.2+ ARM64

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

Uploaded CPython 3.14manylinux: glibc 2.28+ ARM64

zuvloop-0.0.7-cp314-cp314-macosx_11_0_arm64.whl (335.3 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

zuvloop-0.0.7-cp314-cp314-macosx_10_15_x86_64.whl (335.9 kB view details)

Uploaded CPython 3.14macOS 10.15+ x86-64

File details

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

File metadata

  • Download URL: zuvloop-0.0.7.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.7.tar.gz
Algorithm Hash digest
SHA256 f9b11f59ea848a2b03b739e12f501a9fcb03de8fc53a59e4a0a266deff02b214
MD5 3e9605266e75c41b7d70db296dd1a0de
BLAKE2b-256 80b92e578fe9517f94e73c34ba83bfd7fc4e50e3ff0847d1ea0a1581ac0dcbff

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.7.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.7-cp314-cp314-win_arm64.whl.

File metadata

  • Download URL: zuvloop-0.0.7-cp314-cp314-win_arm64.whl
  • Upload date:
  • Size: 417.7 kB
  • Tags: CPython 3.14, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-win_arm64.whl
Algorithm Hash digest
SHA256 54c0afda2237ee45b076b5fbd5807ca87c3c08d417ea68bbe4e4ce7d9b9a157a
MD5 fcb46da92cfc664189dcbd873ba6dad5
BLAKE2b-256 6c38f7a364535e9f8e11db423e7bda470bf50c62143f9e763dd624d4f5133de0

See more details on using hashes here.

Provenance

The following attestation bundles were made for zuvloop-0.0.7-cp314-cp314-win_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.7-cp314-cp314-win_amd64.whl.

File metadata

  • Download URL: zuvloop-0.0.7-cp314-cp314-win_amd64.whl
  • Upload date:
  • Size: 436.1 kB
  • Tags: CPython 3.14, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-win_amd64.whl
Algorithm Hash digest
SHA256 7f49e755a54fae638312d70309b7e4a49fe4369407a76410a3a7bf91f94abb56
MD5 0542269334774c086780a5e40248264b
BLAKE2b-256 febe6afff947be7a0eb315a429ae662ed5773a3da244162c054d74956c61f61a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 6baa61fd2811d4037e25d8901d3f53dcff64797229364213d2a70f267abc6b8c
MD5 0602581f8f6605388af2be83abfef823
BLAKE2b-256 7cad343b7b56af89d63414e56df2743c29bc4cc480b4a7b059ab999885469335

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 b91e171e1e7d213661311f489c292f67296b6a248e2e5bc89503a18f0e35af7e
MD5 0fb5cdbbe0e3be43d4fd245c08b05438
BLAKE2b-256 447f9c2fbfc522751df4dfd51afef24f30f108083668c3c8477461ade837f042

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 a2c45892aee09878fce9af5279a3da7c6f78400cf2b134de43f5a0b6f6d8178f
MD5 730d3080b5045c94fcf8c790a300341a
BLAKE2b-256 118fc075490b09bb7fc1171e0bbf1d326ac78db4760b0a80c2816d7c9e048dd0

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 c1a092f6f835a862174b3339bfed0b72fb2323479affa56a5e02ea5ae1f2c737
MD5 91a7b77cf34698005130f831a5442b08
BLAKE2b-256 94c48e56ba86ed36eadf77b5b893278fc3efc783ef5d7012f49c8cf896e879b1

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 5f2ba70ab65b0402c30b344e70b4b42102418044da3f067f38b86536464b4eca
MD5 2eba84ccc69a8f1544946564595fa810
BLAKE2b-256 10d492238d6cce80ee144dfa09ace8a0ab08db63e520a290e7e6cc3e8d43dd45

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for zuvloop-0.0.7-cp314-cp314-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 c01928ff6358d03f4af0158f2361b51f706fe63a40f6285ab8425e65e76c7738
MD5 15fe9c802bd722064ff633f51bfb5fa3
BLAKE2b-256 374dbc2d07f01a66f7cf0c78d530bbedb190b3ec90a44d31a887f0753452ae5c

See more details on using hashes here.

Provenance

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