about things notes.zzstoatzz.io
README.md

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, await and cancel
  • Io.Queue, Io.Group, Io.Select
  • Io.Mutex, Io.Condition, Io.Event, Io.Semaphore, Io.RwLock, futexes

backends #

  • Io.Threaded: thread pool, available everywhere, the production default and what std.process.Init.io gives main
  • Io.Evented: stackful fibers, void where unsupported. it resolves to Io.Uring on linux, Io.Dispatch on macOS and iOS, and Io.Kqueue on 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);
  • .awake for elapsed delays; .real only when the deadline is a wall-clock time. a Retry-After in seconds is relative; an HTTP-date, or a rate-limit reset given as a Unix time, is absolute and is compared against std.Io.Clock.real.now(io).toSeconds().
  • sleep returns error.Canceled. catch {} turns cancellation into an immediate retry; try lets 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 on io.async, which runs inline past async_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 Io comes from (and why not debug_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: fetch streams into the writer you pass, a redirect-chain panic, certificate time and trust
  • http server: a bodiless POST panics a handler that responds without reading; reuse_address lets 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, IoSource jitter and Retry-After, 2026-09; zttp src/client.zig (RateLimitHeaders.delaySeconds)