Releases: Kludex/zuvloop
Release list
v0.0.12 - Free-threaded event loop scaling
🧵 Free-threaded event loop scaling
zuvloop 0.0.12 replaces process-wide synchronization with per-loop and per-object locking, allowing independent event loops to run concurrently on free-threaded CPython.
pip install zuvloop==0.0.12- Independent loops no longer serialize behind one global lock, improving four-loop throughput scaling from 1.01x to 2.94x (#131).
- Cross-thread callbacks use a protected per-loop inbox, while callback execution and task context switches remain outside the loop lock (#131).
- Performance coverage now includes four concurrent event loops, recording the free-threaded scaling baseline alongside asyncio and uvloop (#130, #131).
Full changelog: v0.0.11...v0.0.12
v0.0.11 - Faster task and ready-queue scheduling
⚡ Faster task and ready-queue scheduling
zuvloop 0.0.11 makes task creation and ready callback chains substantially faster while preserving timer and I/O fairness.
pip install zuvloop==0.0.11- Task creation and ready-only callback chains require less overhead, increasing task throughput from about 648k to 783k tasks/s and
sleep(0)throughput from about 70k to 2.07M iterations/s (#126). - Ready callback chains stay fast while I/O handles remain open, improving the idle-connection workload from 53.2k to 361.2k iterations/s while retaining bounded I/O latency (#128).
- Performance coverage now includes task creation and ready chains with idle connections, and the refreshed comparison shows zuvloop ahead of uvloop on 11 of 12 README benchmarks (#125, #127, #129).
Full changelog: v0.0.10...v0.0.11
v0.0.10 - Python 3.15 support
🐍 Python 3.15 support
zuvloop 0.0.10 adds standard and free-threaded CPython 3.15 wheels on every supported platform.
pip install zuvloop==0.0.10- CPython 3.15 wheels now ship for Linux glibc and musl, macOS, and Windows on x86-64 and ARM64 (#124).
- Free-threaded CPython 3.15 handles the interpreter's object-alignment and static-module initialization changes, with dedicated compatibility and platform CI coverage (#124).
Full changelog: v0.0.9...v0.0.10
v0.0.9 - Free-threaded CPython and runtime hardening
🧵 Free-threaded CPython and runtime hardening
zuvloop 0.0.9 adds supported free-threaded CPython wheels and hardens lifecycle, networking, and cross-thread scheduling paths.
pip install zuvloop==0.0.9- Free-threaded CPython 3.14 wheels now ship for Linux, macOS, and Windows, with explicit critical sections, thread-state handling, atomic reference counting, and dedicated compatibility coverage (#123).
- Loop shutdown races no longer admit new work while resolving DNS, spawning processes, adopting datagrams, or creating poll watchers (#101, #103, #104, #105).
- Networking setup and error paths settle predictably across STARTTLS,
getnameinfo, buffered reads, stream and datagram closure, process exit, and timer delivery (#102, #106, #110, #111, #112, #113, #114, #115). - UDP and resolver behavior aligns more closely with the standard library by delivering connection setup before reads, preserving adopted-socket reuse options, normalizing Unicode hostnames with IDNA, and rejecting embedded NULs (#107, #108, #109, #116, #117).
- Cross-thread scheduling does less work by coalescing wakeups and allocating empty callback contexts only when needed (#119, #120).
Full changelog: v0.0.8...v0.0.9
v0.0.8 - Call graphs for completed slow tasks
🔭 Call graphs survive task completion
zuvloop 0.0.8 keeps slow-callback telemetry useful when the blocked callback is the final step of a coroutine.
pip install zuvloop==0.0.8- Completed slow tasks retain their identity in the
asyncio.call_graphspan attribute instead of falling into the no-call-graph bucket (#98). - The fallback matches the shape of
asyncio.format_call_graph()and includes the task name, completion state, coroutine qualname, and source location. - Task results and exception messages stay out of telemetry. The fallback renders controlled metadata instead of the task repr, which may contain application data.
- Active tasks keep the full standard-library call graph. The fallback only applies after Python has released the completed coroutine's frames.
Full changelog: v0.0.7...v0.0.8
v0.0.7 - Windows support and production quality gates
🪟 Windows support and stronger release confidence
zuvloop 0.0.7 adds native Windows wheels and expands the project's production quality gates across platforms and downstream integrations.
pip install zuvloop==0.0.7- Native Windows wheels are built and tested for AMD64 and ARM64, alongside the existing Linux and macOS wheels.
- Release-wheel validation now exercises the actual cibuildwheel path on both Windows architectures, with scoped build constraints and bounded tests.
- Pull requests have an aggregate quality gate backed by
re-actors/alls-green, covering the platform matrix, native hardening, documentation, and compatibility suites. - Downstream compatibility coverage includes aiohttp, AnyIO, aioquic, HTTPX2, Tornado, Uvicorn, websockets, and supported CPython versions.
- Dependency maintenance is deterministic: aiohttp's test requirements are captured locally for Dependabot-compatible lock updates, including
cryptography50.0.0. - Documentation is aligned with the shipped API and instrumentation dependencies.
Full changelog: v0.0.6...v0.0.7
v0.0.6 - Production readiness and verifiable releases
🛡️ Production behavior you can trust
zuvloop 0.0.6 closes the gap between a fast event loop and one that can be operated, diagnosed, and released with confidence. Exception handling is safer, native behavior tracks CPython more closely, and platform-specific paths now run through much stronger compatibility gates.
pip install zuvloop==0.0.6- Unhandled exceptions always reach the
asynciologger, while OpenTelemetry remains additive and fail-open (#91). Exporter failures, hostile reprs, and broken custom handlers cannot suppress the original diagnostic or destabilize the loop; telemetry strings and recorded exception data are bounded to 4 KiB. - Exception handlers now run in the owning task or handle context, matching Python 3.14's
contextvarssemantics.SystemExitandKeyboardInterruptcontinue to propagate through every defensive reporting layer. - Scheduling handles align more precisely with CPython: timers are real
asyncio.TimerHandleinstances and preserve the original integer or float deadline, native handles expose their context, and thread-safe handles retain cross-thread cancellation behavior. - Networking and process edge cases are stricter and more portable, including address validation, TLS transport checks, named-FIFO lifetime, sendfile file-like objects, Darwin IPv6 scope normalization, musl resolver parity, and subprocess exit races.
- Unsupported free-threaded CPython builds fail at compile time with an actionable message rather than producing a wheel whose GIL assumptions are unsafe.
📦 Verifiable builds and releases
- The native build toolchain and isolated build requirements are exactly pinned and hash-locked; GitHub Actions are SHA-pinned and run with least-privilege permissions.
- cibuildwheel invokes uv directly so hash verification is scoped to the project build without affecting platform bootstrap tools (#93).
- Release preflight verifies that tags belong to
main, exercises native safety checks and CPython conformance, and publishes the exact wheel and sdist artifacts produced by the gated build. - Published distributions receive GitHub artifact attestations plus a CycloneDX SBOM that includes the compiled libuv dependency before trusted PyPI publishing.
- Source distributions bring their pinned Zig toolchain into the isolated build environment and are tested on a host with no system Zig available.
- The aiohttp compatibility environment is locked with hashes, while immutable upstream aiohttp and uvicorn suites prevent dependency drift from changing the test inputs unexpectedly.
🧪 Broader release confidence
- Runtime coverage now includes glibc and musl, checked native builds, sanitizer runs, scheduled soak tests, CPython 3.14.0/current 3.14/3.15 prereleases, and installed-wheel conformance.
- Real aiohttp and uvicorn workloads, property tests, strict native stubs, and 100% branch coverage guard the public behavior users depend on.
- Compatibility, installation, instrumentation privacy, supported platforms, and the release architecture are now documented explicitly.
- README benchmarks were rerun on CPython 3.14.3 and libuv 1.51.0 (#90);
zuvloopleadsuvloopin 10 of the 11 published rows, with the aiohttp client result called out honestly.
Full changelog: v0.0.5...v0.0.6
v0.0.5 - Network resilience and protocol hardening
🌊 Network resilience under pressure
zuvloop 0.0.5 hardens the paths that only become interesting when a server is busy, shutting down, upgrading to TLS, or running out of operating-system resources. It also turns musl and real aiohttp/uvicorn traffic into release gates.
pip install zuvloop==0.0.5- Accept loops now back off on
EMFILE,ENFILE,ENOBUFS, andENOMEMinstead of spinning and consuming a CPU core (#78, #88, #89). Recovery follows asyncio's retry model, remains safe after server closure, and cannot be bypassed by callingstart_serving()orserve_forever()again. - Python socket views are disarmed before libuv closes the native descriptor (#75). This closes a double-close race where another thread could reuse the descriptor number before delayed cleanup ran.
- Failed stream and datagram construction closes adopted sockets exactly once (#80), including protocol-factory failures and partial native adoption.
- Cancelling
serve_forever()now closes connected clients and waits for shutdown (#87), so cancellation no longer leaves accepted connections alive. - UNIX server cleanup always wakes
wait_closed()callers (#84), even when removing the socket path fails. - Datagram protocols stop receiving error and flow-control callbacks after close or abort (#77).
🔐 Protocol and native-boundary correctness
- Server-side
start_tls()carries already-buffered plaintext into TLS (#85). Proxy headers and a coalesced ClientHello can no longer strand handshake bytes in the replaced stream protocol. - Linux abstract UNIX datagram addresses round-trip with their exact identity (#86). Abstract names are exposed as
bytes, unnamed senders asNone, and the required libuv changes are preserved as a patch that is reapplied during vendor updates. - Socket addresses are validated before entering native structures (#81). Embedded NULs and out-of-range IPv6
flowinfoorscope_idvalues now fail in Python instead of being truncated or wrapped. - Invalid write-buffer limits fail before unsigned conversion (#79); valid low-only configurations still derive a safe high watermark.
- Infinite and extremely large timer delays are clamped safely (#76) across scheduling,
wait_for(), and metrics instrumentation. - PID reuse cannot lose a newer subprocess registration (#82) when a stale reaper callback finishes late.
🧪 Release confidence and portability
- aiohttp and uvicorn now run end to end in the normal test suite (#49), covering real protocol, flow-control, large-response, concurrency, and shutdown paths on
zuvloop.EventLoop. - The complete suite now runs under musl, and framework traffic runs under ReleaseSafe (#50). Published Alpine-compatible wheels are tested at runtime instead of only being cross-compiled.
- Timer fairness under sustained
call_soon_threadsafe()traffic now has a recorded CodSpeed baseline (#83) for the follow-up scheduler work. - Release workflows use the latest pinned checkout and artifact actions (#73).
Full changelog: v0.0.4...v0.0.5
v0.0.4 - Security and lifecycle hardening
🔒 Security and ownership hardening
zuvloop 0.0.4 focuses on making native ownership boundaries match asyncio and CPython even under reentrancy, finalization, and subprocess edge cases.
pip install zuvloop==0.0.4- Mutable write buffers are snapshotted before
write()returns (#61). Exactbytesobjects retain the zero-copy path;bytearray, custom buffer exporters, and other potentially mutable inputs can no longer change queued network bytes after submission. - Subprocess strings reject embedded NULs (#59) rather than being silently truncated by libuv's C-string boundary.
- Linux subprocesses preserve
close_fdssemantics (#56). Unrelated inheritable descriptors stay out of children, including whenpass_fdsis used, and one bounded process-wide reaper bridges stdlib child exits back to the event loop. - Signal handlers and the wakeup fd have explicit per-loop ownership (#62). Finalizing an older loop can no longer reset a newer loop's process-global signal state; rollback is safe across deferred-finalizer races.
- Reentrant handle cancellation keeps vectorcall references alive (#64) without adding allocation or refcount work to every callback.
- DNS GC traversal stops when the native resolver reaper owns the requests (#60), removing a native use-after-free race during loop shutdown.
🧭 Asyncio compatibility and lifecycle fixes
- Reentrant
writelines()cannot queue bytes afterwrite_eof()(#67). - A failed metrics sampler start now restores running-loop and async-generator state (#65).
- Connected IPv6 datagram validation includes
flowinfo(#66). - A failed
uv_udp_openreports the original error without reopening a closing handle (#68). - Slow-callback telemetry skips call-graph formatting for completed tasks (#69).
- Self-pipe EOF unregisters the reader instead of leaving the loop in a busy spin (#70).
🏗️ Release and supply-chain controls
- Release builds pin
hatch-vcs(#55), publish only fromv*tags (#57), and do not expose OIDC credentials to pull-request benchmark code (#58). - Vendored libuv archives are checksum-verified (#63); a weekly signature-verifying workflow now tests new releases and opens update PRs (#71).
- CI now enforces
zig fmtand a checksum-pinned ZLint safety ruleset alongside Ruff and strict mypy (#72).
v0.0.3 - sendfile, and telemetry that arms late
📁 sendfile
zuvloop 0.0.3 implements the two loop methods that previously raised NotImplementedError.
pip install zuvloop==0.0.3sendfile()andsock_sendfile()use thesendfile(2)system call (#53) - the kernel moves the bytes, nothing is copied through Python. Forsendfile(transport, ...)the loop first lets the transport's buffered writes drain and pauses reading, so the file cannot reorder around data written before it; readiness is polled through a duplicate of the descriptor, since libuv allows one handle per descriptor.- Targets the syscall cannot serve fall back to a read-and-write loop - a
BytesIO, a TLS transport, a pipe - or raise underfallback=False, matching asyncio. On macOS,sendfile(2)does not serve AF_UNIX sockets, so unix connections take the fallback there transparently (native on Linux).
🔭 Telemetry survives late configuration
- OpenTelemetry providers installed after the loop starts are now noticed (#54). Slow-callback monitoring and the loop gauges were armed once, at
run_forever()entry - an application callinglogfire.configure()from insidemain()silently lost both for the life of the process. The loop's unreferenced native sampler now doubles as the re-check: monitoring arms within onemetrics_interval(10s by default) of a provider appearing, the answer is latched so the probe stops once it says yes, and uninstrumented programs keep the native fast path with no per-callback clock reads. - Slow-callback spans are warnings now, not errors (#54). OpenTelemetry span status has no warning level, so the status stays unset and the severity travels as a
logfire.level_numattribute, which backends that do not know it ignore.
🏗️ Release plumbing
- The version now comes from the git tag (#52) via
hatch-vcs; publishing this release is what stamps 0.0.3 onto the wheels.