std.Io #
since 0.16, everything that can block (filesystem, networking, timers,
concurrency) goes through the std.Io interface. like Allocator for memory,
an Io is passed to the functions that need it:
fn fetchData(gpa: Allocator, io: std.Io) ![]u8 { ... }
the code does not know whether the Io behind it is a thread pool, an event
loop, or a single blocking thread; the backend is chosen once, at init. the
design rationale is in the devlog and Andrew Kelley's async I/O post (sources).
what Io provides #
- file system, networking, processes, memory-mapped files
- clocks and sleeping
- randomness
async,concurrent,awaitandcancelIo.Queue,Io.Group,Io.SelectIo.Mutex,Io.Condition,Io.Event,Io.Semaphore,Io.RwLock, futexes
backends #
Io.Threaded: thread pool, available everywhere, the production default and whatstd.process.Init.iogivesmainIo.Evented: stackful fibers,voidwhere unsupported. it resolves toIo.Uringon linux,Io.Dispatchon macOS and iOS, andIo.Kqueueon the BSDs. it still has production gaps; see patterns.- WASM has no stack switching, so fiber backends cannot work there.
both expose .io(), and everything downstream takes the same Io value.
coloring moved to a parameter #
async and await are library functions, not keywords, so calling a function
synchronously or asynchronously does not change its type:
try foo(io); // called normally
const future = io.async(foo, .{io}); // or asynchronously
the cost moved instead of vanishing: a pure function that later needs I/O must
take an Io, and that parameter propagates to its callers.
sleeping in retry loops #
a retry delay sleeps on the same Io that made the request, which keeps it
cancellable and backend-agnostic:
try io.sleep(std.Io.Duration.fromMilliseconds(delay_ms), .awake);
.awakefor elapsed delays;.realonly when the deadline is a wall-clock time. aRetry-Afterin seconds is relative; an HTTP-date, or a rate-limit reset given as a Unix time, is absolute and is compared againststd.Io.Clock.real.now(io).toSeconds().sleepreturnserror.Canceled.catch {}turns cancellation into an immediate retry;trylets the loop unwind.- jitter can come from the same
Io:std.Random.IoSource{ .io = io }, so the retry policy has no separate global random source.
const source: std.Random.IoSource = .{ .io = io };
const delay = half + source.interface().uintAtMost(u32, half); // equal jitter
files in this folder #
- concurrency: async against concurrent,
Future,Group,Select,Queue. read it before building a timeout or watchdog onio.async, which runs inline pastasync_limit(always, on one CPU) and silently disables a deadline built by racing a future. - synchronization:
Mutex,Condition, timed waits, cancellation, and why one mutex cannot span two backends - patterns: backend selection, where the
Iocomes from (and why notdebug_io), pool sizing, long-lived tasks, Evented in production - fibers: building stackful fibers, the ReleaseSafe clobber bugs, guard-page stacks, keeping blocking work off fibers
- http client:
fetchstreams into the writer you pass, a redirect-chain panic, certificate time and trust - http server: a bodiless
POSTpanics a handler that responds without reading;reuse_addresslets a stale process share the port
sources #
- zig 0.16.0 and 0.17.0
lib/std/Io.zig,lib/std/Io/Threaded.zig,lib/std/Random.zig(IoSource) - devlog, andrewkelley.me, kristoff.it, porting guide
- tpuf
src/Client.zig: retry loop with cancellable sleep,IoSourcejitter andRetry-After, 2026-09; zttpsrc/client.zig(RateLimitHeaders.delaySeconds)