Changelog
All notable changes to this project will be documented in this file.
[0.17.0] - 2026-08-05
-
Work-stealing is fully wired up now. Idle executors publish an
idle_maskand coordinate through a single-token searcher count; a newly-idle executor first does a short steal-free doze (100us) on the theory that an I/O completion will re-ready the tasks that just ran there, and only escalates to scanning other executors' local queues and stealing half a loaded victim's backlog after that. A pusher waking a sleeper can leave a "steal hint" pointing straight at the loaded ring instead of a random scan, and draining many ready tasks at once now wakesceil(log2(n))sleepers instead of one per task. An overloaded executor also sheds new wakes to the global queue, covering the case where I/O-woken tasks are re-run so quickly that a stealer never gets a chance to claim them; the doze and steal-hint work above already narrows how often that's needed. -
Replaced the fixed 61-task scheduling quantum with an adaptive, time-based one, targeting ~100us of task time between I/O polls using an EWMA of recent quantum costs. A cheap clock checkpoint every 127 ticks (prime, to avoid resonating with periodic workloads) catches a mispredicted budget mid-batch.
maybeYield()is now a pure time-slice check instead of a ready-queue-length threshold, and always checks cancellation on its fast path. -
Linux now auto-selects io_uring with an epoll fallback, instead of io_uring being the only option. The choice is made once (behind a mutex, so a loop group can't split between engines) and only falls back on
SystemOutdated/PermissionDenied/ArgumentsInvalidfrom ring setup - the case that had no recovery before, e.g. containers with seccomp-restricted io_uring or old kernels. -
The io_uring backend now submits
bind/listenas native SQEs on kernels that support it (Linux 6.11+), probed once at startup. It also gained a real zero-copysendfilevia a splice/pipe chain instead of falling back to the generic read/write loop. -
sendfileon kqueue is now a nativesendfile(2)implementation, but only on FreeBSD. Darwin'ssendfileis synchronous with respect to disk reads and would block the loop, so Darwin and other BSDs keep the generic fallback. -
Mutexis rewritten around an explicit atomic state word instead of a flag in the wait queue, switching from lock-handoff to barging: woken waiters now compete for the lock instead of being handed direct ownership, so a transfer no longer serializes behind the scheduler. Foreign-thread callers still block directly on the state word via a platform futex, as before. -
RwLockis rewritten around a single lock-free atomic state word plus a semaphore. Uncontended readers acquire with one CAS and never touch the internal mutex; writers wake via a semaphore post from the last departing reader instead of a broadcast condvar. -
Added 32-bit x86 (IA-32) coroutine context-switching support, rounding out the set of architectures zio's hand-written context switch covers.
-
OpenBSD is now a supported platform. Its coroutine stacks are mapped
MAP_STACKand fully committed up front, since OpenBSD requires every stack pointer the kernel sees to fall inside aMAP_STACKmapping, and that flag can only be set atmmaptime, not added later withmprotect. That rules out zio's usual lazy on-demand growth on this platform: an OpenBSD stack doesn't grow past its initial reservation, and overflowing it faults on the guard page instead. -
The coroutine stack pool is rewritten from per-stack
mmapallocation with count/age eviction to slab-based allocation with a demand-driven watermark. Stacks are now carved out of largemmap'd slabs (64 slots each by default) instead of getting an individualmmap, and releasing a stack no longer makes any syscalls at all - eviction moved to a periodic pass that tracks peak concurrent usage and decays toward it, unmapping whole empty slabs before falling back to individual stacks.RuntimeOptions.stack_pool'smax_unused_stacks/max_ageoptions are replaced byshrink_interval,slab_slots, and a newprewarmoption to commit stacks up front. 32-bit, Windows, OpenBSD, and WASI keep the old per-stack path, since slabs need aPROT_NONE-reserve-then-grow scheme those platforms can't use. -
Fixed a crash destroying a coroutine's TSan fiber if it never ran (e.g. a task created and torn down before it got to execute). Works around an upstream LLVM bug (fixed in LLVM 22, not yet in the LLVM 21.x that Zig 0.16 bundles): destroying a fiber with no recorded trace event corrupts TSan's own bookkeeping.
-
The DNS resolver cache no longer caps entries at 6 addresses. Addresses are now stored in linked 4-address chunks from a shared pool, so one entry can hold up to 128 addresses without inflating every other slot, and
put()can no longer fail - a reclaim pass evicts other entries if the pool runs short. -
DNS lookups no longer return
error.TooManyAddresses; results are truncated instead and marked with a newQueryResult.truncatedflag, and truncated results are never cached. Dual-stack answers are now interleaved IPv6-first (RFC 6724) before caching, so a cache hit and a fresh lookup return addresses in the same order. -
Added
Dir.createFileAtomic()/fs.createFileAtomic(), returning anAtomicFileyou write to and then finalize with.link()(fails if the destination exists) or.replace(). If never finalized, the temp file is cleaned up ondeinit(), including under task cancellation. -
currentPath/setCurrentDir/setCurrentPath,File.isTty/supportsAnsiEscapeCodes, and file seeking are now implemented natively instead of delegating to a throwawayIo.Threadedinstance per call. Windows'isTtynow also recognizes an MSYS2/Cygwin pty, which isn't a console handle and was previously misreported as not a tty. -
Socket.setReuse()is removed.reuse_addressandreuse_portare now independent options onIpAddress.ListenOptions/BindOptionsinstead ofSO_REUSEPORTbeing bundled intoreuse_address. -
Added
Loop.Options.do_not_call_callbacksandLoop.nextDispatched(), for embedding zio in a foreign event loop: finished completions are queued instead of invoked inline, so the embedder can drain and invoke them after reacquiring a lock it dropped for the poll (e.g. Python's GIL). -
Loop.run(mode: RunMode)is replaced byLoop.run()(always runs to completion) andLoop.poll(wait_cap: Duration), which takes an arbitrary cap instead of an all-or-nothing enum. -
zio now installs a do-nothing
SIGPIPEhandler at runtime init (refcounted across overlapping runtimes, not inherited acrossexecve) if the disposition is still default. Zig 0.16 moved SIGPIPE-ignoring intostd.Io.Threaded.init, which zio doesn't use, so writes to a peer-closed socket would otherwise kill the process instead of returningerror.BrokenPipe. -
Added opt-in scheduler metrics (
zio_options.scheduler_metricsbuild option) and aRuntimeOptions.metrics_log_intervalthat spawns a dedicated monitor thread to log them, deliberately not tied to an executor timer so it keeps logging even if every executor is wedged or asleep. -
Added
withTimeout(timeout, func, args), a scoped form ofAutoCancel: it arms a timer around the call and, if the call returnserror.Canceledbecause this timeout (rather than an external cancel) fired, rewrites it toerror.Timeout.WithTimeoutResultcomputes the right return type - the function's own error set plusTimeout- and nestedwithTimeoutcalls each correctly report whichever deadline actually fired. -
Fixed a race in
AutoCancelwhere a task that had migrated to another executor and was running (not parked) when its timer fired could observe the cancellation before the timer's own "I did this" flag was set, misreporting an auto-cancel timeout as a plain user cancel. -
Fixed a use-after-free in
AutoCancel.clear(): if the timer was already mid-fire,clear()had no way to know its callback was still touching the (often stack-allocated)AutoCancelstruct, and could return while the callback was still live.clear()now waits for an in-flight callback to finish before returning. -
Waiter's timed wait now returnserror.Timeoutexplicitly instead of returning success and leaving the caller to infer a timeout by rechecking its own condition, an easy-to-misuse contract that also let a wake that was already queued but not yet delivered read as a spurious timeout inCompletionQueue. -
I/O completions now deliver through the executor's dispatch queue instead of a direct callback, and an op that completes inline always charges the cooperative scheduling budget now, closing a gap where a task whose I/O always completed immediately could run indefinitely without ever hitting a yield point.
-
Fixed a cross-thread race between a firing timer and a concurrent
clearTimer(e.g. a migrated task clearing its own sleep timer from another loop's thread) that could double-decrement the completion counter or clobber a result an assert relies on.clearTimernow returns whether it actually reclaimed the timer. -
Consolidated completion lifecycle into a single atomic state word (phase + cancel flags), replacing a plain enum plus a separately-mutated cancel state. Closes an entire class of cross-thread lifecycle races, not just the timer one above.
-
Fixed a race in
Condition/Futex/Notify/ResetEvent's timed wait: when a wake landed at the same moment its own timer fired, the wait still reportederror.Timeoutto the caller after quietly consuming the wake internally. That case is now reported as a successful wake instead of a timeout. -
Fixed two scheduler races around a task's
awakenbit. Inyield's cancel path, the state was blindly overwritten back to plain.readyon the way out, erasing a concurrently-set awaken token. InscheduleTask, the wake CAS used to skip itself entirely once the awaken bit was already set, but a coalescing waker still needs to join the release sequence onstate, or a payload published just ahead of a duplicate wake isn't guaranteed visible to the eventual reschedule. -
Fixed a race between
Async.notify()and the loop registering the handle: both sides decide who wakes whom from the samependingflag, and a plain.releasestore on the notify side wasn't enough to guarantee the two sides agreed on it, occasionally dropping the wake instead of either side handling it. -
Fixed a race on Windows where a blocking-executor fast path assumed a blocking-mode socket handle for recv/send/accept, but accepted sockets are nonblocking by default - a recv issued before the peer's send could spuriously fail instead of waiting.
-
Fixed a double-complete race in kqueue/epoll socket cancellation: canceling a parked op could race the owning loop's
service()finishing the same op naturally on another thread.sockreg.detachnow returns whether it actually still owned the op. -
Fixed silently-dropped wake failures on the kqueue and poll backends. A lost wake left the loop thread stuck until its poll timeout (potentially indefinitely for parked ops); both backends now retry
EINTRand panic on anything else instead of swallowing it. -
ThreadPoolreservations are now additive tomax_threads, guaranteeing a worker picks up a reserved job even when the pool is saturated with blocked workers, fixing a potential deadlock when a job is queued behind workers waiting on it (#567). -
Fixed a task creation ordering bug in
spawnTaskandspawnBlockingTask, which registered a task with its group before taking their own reference, letting a concurrentGroup.cancel()free the task out from under the spawning code. -
Group.wait()no longer closes the group. Previously, waiting once left the group permanently closed, so spawning into it again failed witherror.Closed- or, through thestd.Iovtable, silently ran the work synchronously instead of async. A group can now be spawned into and waited on repeatedly; only a failure or acancel()withfail_fastset closes it for good. -
Fixed a shutdown race where a worker's loop could be torn down, closing its waker fd, while a cross-thread notifier was still mid-syscall writing to it.
-
Fixed a real truncation bug in the
lseekwrapper on 32-bit platforms, where a 64-bit resulting offset was truncated tousizeinstead of reported in full. -
Fixed
renamePreserve's hardlink-then-delete fallback silently swallowing delete errors, which could leave both the old and new name pointing at the same data with no error reported. -
Spawned child processes now inherit the parent's environment.
-
Fixed
HostName.validatechecking the 255-byte length limit after stripping the trailing FQDN dot, letting a name that's actually 256 bytes pass validation. -
Fixed ThreadSanitizer false-positive races on coroutine stack reuse: raw
mmap/munmapsyscalls are invisible to TSan's shadow memory, so a recycled stack address looked like a race between unrelated coroutines. Now routed through libc's wrappers under-fsanitize-thread. -
NetBSD switched from bespoke
_lwp_park/_lwp_unparksynchronization to the same generic futex-based path used elsewhere, removing ~150 lines of platform-specific code (requires NetBSD 10+).
[0.16.0] - 2026-07-12
-
Blocking operations running on thread-pool workers are now cancelable. Previously, canceling a task stuck in a blocking syscall had to wait for the syscall to finish; now the worker is interrupted with
SIGURGand the operation returnserror.Canceled. This coversblockInPlaceand all file/directory operations that are delegated to the thread pool on backends without native async file I/O (kqueue, poll), including path-based metadata operations and directory reads.getaddrinfoitself cannot be interrupted, but a cancelation requested while the lookup was still queued is now honored before it starts. On Windows/WASI this degrades gracefully: queued-but-not-started work can still be canceled, in-progress syscalls run to completion as before. -
Added a
directflag toFileOpenFlags/FileCreateFlagsfor direct I/O, bypassing the OS page cache (O_DIRECTon Linux,fcntl(F_NOCACHE)on macOS,FILE_FLAG_NO_BUFFERINGon Windows). The caller is responsible for meeting the platform's alignment requirements for buffers, offsets and transfer lengths. -
Added
Dir.iterate()for native directory iteration, returning entry names and file kinds. The directory must be opened with.iterate = true. -
Added
Dir.deleteTree()for recursively deleting a file or directory tree, ported fromstd.Io.Dir. Symlinks are removed, not followed. There is alsoDir.deleteTreeMinStackSize(), a slower variant that keeps only one directory iterator open at a time to minimize memory usage. -
Groupcan now be used withzio.select()andzio.wait(). The group completes when its pending-task counter drains to zero. UnlikeGroup.wait(), this does not close the group and does not participate in fail-fast handling, so you can race a group against other futures and keep using it afterwards. -
Added a top-level
zio.maybeYield(), a cheap fairness check for long CPU-bound loops: it yields only when enough other tasks are waiting on the current executor, and is a no-op when called from a thread without an executor. -
The io_uring backend now shares one kernel async worker pool across all executor rings via
IORING_SETUP_ATTACH_WQ, instead of each ring creating its own. -
Task migration support can now be compiled out with the
task-migrationbuild option (default on).RuntimeOptions.enable_task_migrationnow defaults to whether support is compiled in, and enabling it at runtime in a build without support fails at init witherror.TaskMigrationNotCompiledIn. Compiling it out removes the atomics that only exist to support cross-thread task movement. -
Replaced the per-executor run queue with a fixed-size ring buffer modeled on the local run queues in Go and Tokio, spilling into a shared overflow queue when full. This is the first phase of work-stealing; no stealing happens yet.
-
RuntimeOptions.executors = .autonow honors the CPU limit of the current cgroup on Linux, in addition to the CPU affinity mask. Container CPU limits (Docker--cpus, Kubernetesresources.limits.cpu) and systemdCPUQuota=are enforced via the CFS bandwidth controller, which is invisible tosched_getaffinity(). Without this,.autowould size the executor pool to the host's CPU count and get throttled by the scheduler. The effective count is nowmin(affinity, ceil(quota/period)), mirroring Go's container-awareGOMAXPROCSdefault. -
Reduced locking in the event loop's timer processing: ticks with no due timers now skip the timer mutex entirely, and futex waits without a timeout skip the timer setup and one bucket-lock acquisition per wait.
-
Fixed the coroutine context switch to clobber vector registers (
xmm/ymmon x86_64, NEON on aarch64, RVV on riscv, LSX/LASX on loongarch64). The clobber lists only named the widest registers (e.g.zmmon x86_64), relying on them aliasing the narrower ones, but when the target CPU lacks the feature (e.g. AVX-512), LLVM silently drops such clobbers instead of applying them to the aliased registers. The compiler was then free to keep a vector value live across a context switch and read back another task's data. This surfaced on Windows, where the calling convention keeps values in callee-savedxmmregisters. -
Fixed possible stack corruption in the IOCP backend on Windows. Overlapped I/O submissions passed the kernel pointers to stack-local out-parameters, which the kernel writes at completion time, after the submitting frame is gone. The out-parameters now live next to the
OVERLAPPEDfor the whole operation. -
Fixed
File.setSizeon the io_uring backend with kernels older than 6.9, whereIORING_OP_FTRUNCATEis not available. The opcode is now probed once at startup and older kernels transparently fall back to the thread pool. -
Fixed panic messages not being printed when panicking from scheduler code or signal handlers with
debug_ioenabled. I/O performed outside of a task context now takes a blocking path instead of re-entering the event loop, which would previously abort before writing the message. -
Added instrumentation for ThreadSanitizer, so that it recognizes our custom fiber context switching. You can now use
-fsanitize-threadto detect data races across coroutines. -
Fixed a use-after-free in the event loop when the last operation in a completion group finished. The group owner's callback, which may free the group members, ran before the completed member's own callback, so the member was accessed after being freed, crashing the process.
[0.15.0] - 2026-07-02
-
Overhaul of the
epollandkqueuebackends, to make them comparable to the performance of the io_uring backend. When migrating from libxev to our own event loop, I decided to use a similar approach for both backends, which really goes against the nature of these APIs. With this new rewrite, both backends keep fds registered in the kernel, so readiness is always available. This results in far fewer syscalls, and overall better performance. One side effect is that now tasks that were running on executor A can be moved to executor B, if the event loop B is where the fd is registered. -
Improved performance of
net.Stream.Writer.sendFileon all platforms. There is now a native zero-copy implementation for Windows usingTransmitFile, and the generic fallback now uses the entire reader/writer buffers, so it's always faster than the read/write loop fallback implemented instd.Io.Writer. -
Added
File.stdReader/File.stdWriterto wrap a zio-opened file as the concretestd.Io.File.Reader/std.Io.File.Writertypes, so it works withstd.IoAPIs that require them (likestd.Io.Writer.sendFileAll). -
Implemented wall-clock timers, so you can now sleep/timeout using the real-time clock and be woken up exactly on time, even if the clock is adjusted. This is natively supported on Linux, but needs more careful coordination on other platforms.
-
Added support for all clocks that
std.Iosupports (real,boot,awake, and thecpu_process/cpu_threadCPU-time clocks), as well as querying their resolution. -
Changed how
stdin/stdout/stderrare handled on Windows, to make sure we can work with these without blocking the event loop, since they are not open asOVERLAPPEDhandles. -
Changed the
io_uringbackend from futex-based wake ups toeventfd, which works much more reliably. The previous futex approach introduced wake up latency that I could not explain. -
Error code
ETIMEDOUTis now mapped toerror.ConnectionTimedOutfor send/recv operations. We are not using kernel-level socket timeouts, but it seems that these error codes can still happen. -
New
TaskLocalAPI for storing custom task-local data. -
Added custom
randomandrandomSecureAPIs for generating random numbers, to reduce dependency onstd.Io.Threaded. -
Fixed handling of Unix socket addresses containing null bytes.
-
Fixed race in cross-thread handling of
AcceptExcalls on Windows. -
Fixed shutdown sequence to properly stop the thread pool before closing the event loop.
-
Fixed memory leak that happens after spawning blocking tasks on the thread pool.
0.14.0 - 2026-06-08
Added
- Implemented
sendFilefornet.Stream.Writeron all platforms, for now just using generic code that does concurrent reads and writes. Platorm-specific improvements for Linux, FreeBSD and Windows will be added later. - Added support for opening/creating files with
resolve_beneathon Linux, macOS, and FreeBSD. By default, the operation will fail on platforms that don't support it. You can disable it using theresolve_beneath_modebuild option. - Implemented support for
renamePreserveon macOS. - Implemented file locking on all platforms.
- Added
zio.Mutex.Recursivethat works in both blocking and non-blocking contexts. - Added support for
pub const std_options_debug_io = zio.debug_ioin your root module, for integration withstd.log,std.debug.printand also the defaultpanichandler.
Changed
- Setting
max_threads = 0in the thread pool options now disables the thread pool, executing blocking work inline on the calling thread (the same behavior as a single-threaded build). - Re-enabled task migration by default, so for example unlocking mutex will schedule the blocked task waiting on the mutex on the same thread, avoiding cross-thread wake up.
- Streaming file reads/writes now auto-detect the file type and use the appropriate method for async operations. This only affects macOS/BSDs on Linux with the epoll backend. Regular file reads/writes are still going through the thread pool, but pipes can go through the event loop.
Fixed
- Fixed cross-thread I/O cancelation on kqueue backend.
- Fixed internal I/O opertion accounting on the IOCP backend that could lead to integer underflow in multi-threaded mode.
- Fixed mapping of
ESPIPEtoerror.Unseekableto helpstd.Io.File.Readerwith mode detection. - Fixed macOS-specific
deleteFileerror mapping, to returnerror.IsDirwhen the path is a directory. - Fixed handling of
follow_symlinks,path_only, andallow_cttyfile open/create flags.
0.13.0 - 2026-05-31
Added
- Built-in async DNS resolver on Linux (io_uring backend), replacing
getaddrinfo. Reads/etc/hostsandresolv.conf, supports search domains, CNAME following, parallel A/AAAA queries, EDNS0, TCP fallback for large responses, response caching, and deduplication of concurrent identical lookups. Enabled by default on io_uring; opt-in viaRuntimeOptions.dns.custom_resolver. Runtime.initStaticfor stack-allocated or externally-ownedRuntimeinstances that don't need a heap allocation.- Single-threaded build support (
single_threaded = true).
Changed
- BREAKING: DNS lookup API changed from an iterator (
Resultwithnext()/deinit()) to a caller-supplied buffer (lookup(&storage, options)returning a count). Eliminates the allocation and the need to rememberdeinit. - BREAKING:
BroadcastChannel.subscribe()now returns aConsumervalue instead of taking a pointer, andunsubscribe()is gone — consumers no longer need to be unregistered. HostNamenow accepts numeric IPv4 and IPv6 addresses in addition to DNS names.- io_uring: when the submission queue is full, operations are queued internally and retried on the next loop iteration instead of failing the caller.
- Coroutine stacks are now periodically evicted from the pool when they exceed
max_age, reclaiming virtual memory that would otherwise accumulate during idle periods.
0.12.1 - 2026-05-22
Added
- Added sparc64 coroutine context switching (untested) (#398)
Fixed
- Fixed io_uring event loop hanging when an I/O wait is registered while still single-threaded and executor threads are subsequently started (#402)
0.12.0 - 2026-05-19
Added
std.Io: batch operations now support concurrent execution and timeouts (#387, #388)
Fixed
- Fixed possible deadlock in
RwLock.unlockShared(#395) - Fixed sockets not opened in non-blocking mode on the epoll backend (#392)
- Fixed integer overflow when using
.executors = .autoon machines with 64+ CPUs (#390) - Fixed coroutine stack allocation size doubling on POSIX (#386)
0.11.0 - 2026-05-11
Added
std.Iointerface is now essentially complete. All major operations are implemented:- Spawn and wait on child processes, with non-blocking pipe I/O on POSIX.
- Iterate over directory entries.
- Create nested directory paths.
- Create files atomically (write to temp file, then rename into place), with optional
make_pathandreplacesupport. - Rename files without overwriting existing destinations.
- Batch multiple file I/O operations for linear execution (concurrent execution is deferred).
Stream.Reader.fromStdandStream.Writer.fromStdconvertstd.Io.net.Streamto zio's buffered reader/writer, enabling seamless interop between zio and std networking in the same program.
Changed
net.Stream.Readerandnet.Stream.Writerare now lighter, storing only the socket handle instead of the full stream.
Fixed
- Fixed a critical bug on Linux with the epoll backend where non-blocking network reads and writes could
silently succeed with garbage data instead of returning
error.WouldBlock.
0.10.0 - 2026-04-26
Added
- Support for Zig 0.16.
- Implementation of the
std.Iointerface. Supports fiber-based futures/groups, file and network operations. Still missing child process and batch operations. The rest of the codebase will be adjusted over time to align withstd.Ioto avoid some unnecessary type conversions.
Changed
server.accept()now takes options argument with timeout.
Fixed
- Internal refactoring to handle data races on weakly ordered architectures in some cases.
0.9.0 - 2026-03-02
Added
- Fully asynchronous DNS resolver on macOS and Windows using their native APIs.
- Added support for 64-bit PowerPC CPUs.
- Added
RwLockfor async readers-writer locking. - Added
Timestamp.fromSeconds()andtoSeconds()for second-based conversions. - Added
Timestamp.untilNow()to get the duration elapsed since a timestamp.
Changed
- Removed unused
JoinHandle.cast()method.
Fixed
- Fixed incorrect assert that could panic on a race between task finishing naturally and being cancelled.
- Added some extra clobbers to context switching asssembly, already implicitly covered by others, but for consistency.
0.8.2 - 2026-02-17
Fixed
- Fixed dependency loop compilation error when using zio as a dependency module, by inlining
Work.WorkFnandWork.CompletionFntype aliases.
0.8.1 - 2026-02-17
Added
- Added
blockInPlacefor running blocking functions on the thread pool without allocations. - Added
os.thread.yield()for yielding to the kernel from OS-level threads.
Changed
- Removed LIFO slot optimization in the coroutine scheduler, to simplify the code while planning to rework the scheduler.
- Added check that prevents coroutines from being called multiple times per one event loop iteration.
- Internal refactoring of our
WaitQueueprimitive, to better express the semantics we need for synchronization primitives likeMutexorCondition. - Internal refactoring of our
Waiterprimitive, avoiding indirect function calls and more direct integration withselect.
Fixed
- Fixed error returned from
Grouptask closing the group, even if not in fail-fast mode.
0.8.0 - 2026-02-09
Added
- Added
CompletionQueuefor waiting on multiple I/O operations. - Added blocking I/O support for socket, pipe, poll, timer, and work operations. These operations can now be called from any thread without an async runtime.
Changed
- Improved our CI setup, run significanly more tests in multi-threaded mode to catch possible race conditions.
Fixed
- Fixed task migration race condition that could cause crashes under heavy multi-threaded load.
- Fixed pipe read/write using wrong offset in io_uring backend.
- Fixed NetBSD test failures.
0.7.0 - 2026-02-06
Added
- Added CI for 32-bit ARM/Thumb and RISC-V CPUs to make sure these don't break.
Changed
- BREAKING: Removed
rtparameter from most functions. It's no longer needed. You can now usezio.spawn,zio.sleep, orzio.yieldinstead of calling them asrtmethods. - Synchronization primitives like
Mutex,ConditionorChannelcan be now used from any thread, outside of coroutines, or across multiple runtimes. DirandFileI/O operations can be now called from any thread and then will run regular blocking syscalls.- Internal: Update our user-mode futex implemenentation to a global hash table, to allow it to be used from any thread.
- Internal: Replaced
std.Threadsynchronization primitives with custom OS wrappers.
0.6.0 - 2026-01-31
Added
- Added support 32-bit ARM/Thumb and RISC-V CPUs
- Added
Pipeto explicitly support streaming-only file descriptors (#267) - Added
Socketmethods for configuring OS-level buffer sizes (#243) - Added custom panic handler that fully extends stack before calling the default handler
- Added convenience
fromXxx()methods toTimeout
Changed
- All timeout parameters now accept
Timeoutinstead ofDuration(#238, #239) - Increased default stack committment to 256KiB to avoid stack overflows in the default panic handler
- Internal refactoring to reduce memory usage and binary size
Fixed
- Fixed possible race condition between
Channel.closeand task cancelation
0.5.1 - 2026-01-25
Added
- Added
readVecandwriteVecmethods toStream(#236) - Added custom panic handler to avoid stack overflow during panics (#237)
Changed
- Made
ResetEvent.resetidempotent (#235)
0.5.0 - 2026-01-24
This is a major release with many changes. It has been in development for a while, but I finally decided to release it.
First of all, the codebase has been relicensed under the MIT license.
I replaced libxev with a custom I/O event loop, that has better cross-platform support,
natively supports multiple threads each running its own event loop, supports more filesystem operations,
consistent timer behavior across platforms, grouped operations, and more. This is avialable in zio.ev and
can be also used separately from the rest of the library. This switch was motivated by with Zig 0.16 which
removed a lot of lower-level I/O APIs, so it was hard to upgrade libxev, but in the end, I'm glad I did it.
The new event loop is more feature complete, more efficient, and more flexible.
The coroutine library has also been restructured, and it's now available in zio.coro.
I've added support for riscv64 and loongarch64 CPUs. Stack allocation has been completel rewritten,
it now properly allocates vitual memory from the operating system, marks guard pages and we also have
signal handlers for growing the virtual memory reservation on demand. Coroutines now start with 64KiB
of stack space, and grow dynamically as needed.
The zio.select() function has been completely rewritten, and now support comptime-based support
for waiting on things other than tasks. For example, you can use it to race two channel reads,
or add timeout support to any operation that doesn't handle timeouts natively.
There is now zio.AutoCancel for automatically cancelling the current task after a timeout.
This is useful when you want to call an arbitrary function that may take a long time to complete,
and you want to make sure it gets cancelled if it doesn't complete in a timely manner, for example,
in HTTP request handlers.
Many networking APIs now have direct timeout support. Additionally, in zio.net.Stream.Reader and
zio.net.Stream.Writer, you can call setTimeout() and it will make sure the underlaying
std.Io.Reader or std.Io.Writer doesn't block for too long. This is similar to
POSIX socket read/write timeouts, but also supports absolute deadlines.
Many new APIs have been added, for compatibility with the future std.Io API.
Internally, I've done a lot of refactoring to prepare for a future scheduler replacement. I've started with project with an event-loop-per-thread model, and I still think it's the better approach for servers, but I'm slowly migrating to a hybrid model, where tasks primarily stick to the thread they were created on, but also can be freely moved to other threads, when it's beneficial for load balancing.
0.4.0 - 2025-10-25
Added
- Extended runtime to support multiple threads/executors (not full work-stealing yet)
- Added
Signalfor listening to OS signals - Added
NotifyandFuture(T)synchronization primitives - Added
select()for waiting on multiple tasks
Changed
- Added
zio.net.IpAddressandzio.net.UnixAddress, matching the futurestd.netAPI - Renamed
zio.TcpListenertozio.net.Server - Renamed
zio.TcpStreamtozio.net.Stream - Renamed
zio.UdpSockettozio.net.Socket(Socketcan be also as a low-level primitive) join()is now uncancelable, it will cancel the task if the parent task is cancelledsleep()now correctly propagateserror.Canceled- Internal refactoring to allow more objects (e.g.
ResetEvent) to participate inselect()
Fixed
- IPv6 address truncatation in network operations
0.3.0 - 2025-10-16
Added
Runtime.now()for getting the current monotonic time in millisecondsJoinHandle.cast()for converting between compatible error sets- Exported
BarrierandRefCountersynchronization primitives
Changed
- BREAKING: Renamed
QueuetoChannelwith channel-style API - BREAKING:
JoinHandle(T)type parameterTnow represents the full error union type, not just the success payload - Updated to use
std.net.Addressdirectly - Internal refactoring to prepare for future multi-threaded runtime support (executor separation, unified waiter lists, improved cancellation-safety)
Fixed
- macOS crash in event loop (updated libxev with kqueue fixes)
0.2.0 - 2025-10-10
Added
- Cancellation support for all task types with proper cleanup and error handling
BarrierandBroadcastChannelsynchronization primitivesFuture(T)object for task-less async operations- Stack memory reuse and direct context switching for better performance
- Thread parking support for blocking operations
Changed
JoinHandle(T)type parameterTnow represents only the success payload, errors are stored separately- All async operations can now return
error.Canceled - Increased default stack size to 2MB on Windows due to inefficient filename handling in
std.os.windows
Fixed
- Windows TIB fields handling and shadow space allocation
- Socket I/O vectored operations and EOF translation
- Context switching clobber lists for x86_64 and aarch64
0.1.0 - 2025-10-05
Initial release.