Zig 0.16.0 introduces a comprehensive async IO interface with primitives including Futures (for task-level abstraction with async and concurrent variants), Io.Group (for managing multiple concurrent tasks), Io.Select (for waiting until one task completes first), Io.Queue (an MPMC channel for inter-task communication), timers for deadlines, and Io.Batch (for low-level operation-level concurrency), all designed to enable efficient asynchronous programming with proper cancellation and error handling mechanisms.
Deep Dive
Prerequisite Knowledge
- No data available.
Where to go next
- No data available.
Deep Dive
A Practical Guide to Async IO in Zig
Added:Now, the very sneaky thing about the io.select is if you go looking around it, and if you look at the standard library code, you realize that it's just a very lightweight abstraction around the io.group and io.queue. All right, so here though, this video is going to be a practical guide to the new async io interface in Zig 0.16.0.
We'll be going through some of the new primitives that they've added and how they could be used, and we'll also be diving deep into some of the implementation details of those things.
So, yeah, it's going to be a lot of fun.
Let's get to it.
So, to start with this, we'll be looking at the release notes on Zig 0.16.0.
Uh, when we're looking at the io as an interface section, uh, essentially, what it's saying is, uh, from 0.16.0, every function that has any sort of, uh, control flow breaking characteristics or non-determinism, which can come either from, uh, io or time outs or anything like that, would have to use an io interface in its parameters.
So, essentially, uh, there are a few of the few of the implementations. The major one is io.threaded, which is based on threads.
Um, and this is also the default implementation that you [clears throat] would get with, uh, the juicy main implementation. So, if you're using something like const io.any.io, it's using the io.threaded implementation by default.
Um, there is one another implementation, which is io.evented, which is currently in beta.
It So, it it's currently using the m n threading, uh, which is pretty similar to go routines in, uh, golang.
>> [clears throat] >> Or stuck with go routines, it would be using, uh, io_uring in Linux and Grand Central Dispatch.
So, potentially, it's, um, it's a lot more performant, um, in certain scenarios because it it would use those core routines instead of using the actual threads.
But since it's not available, we'll be talking about i.threaded which is the stable implementation.
>> [clears throat] >> So if you go a little bit down here, you'll notice that it gives us a bunch of primitives to build on top of.
The very first one is future.
>> [clears throat] >> So if you go here, futures task-level abstraction based on functions.
Now, there are two things.
i.async and i.concurrent. Now there's a big gotcha here.
These two things >> [clears throat] >> they could be a little bit confusing because i.async might not be fully asynchronous.
So what I'm saying is both of [clears throat] these return futures.
But the thing is i.async might not be truly asynchronous and sometimes when the thread pool is kind of exhausted with other tasks, it could simply run in line.
So >> [clears throat and cough] >> in order to drive this thing home, I'll show you a bunch of examples.
So here, we have a very very minimal example. We have a function which takes n and returns n square and does i.sleep just for the sake of it because I want to prove a point here.
Um so for example, we use var a which is i.async.
It just It just needs two things which is the function and the arguments which the function needs.
Now the thing is this thing might not run completely asynchronously and it could just run synchronously and the performance of this thing could would be pretty similar if we had called io this and six here.
Yep. So the The is this thing could end up performing the same as try square, which is essentially a synchronous operation.
Um whereas if you use the io.concurrent, it would [clears throat] be truly concurrent. Now, note one thing.
We can just call io. async and it would just return us a future.
But, when you call io.concurrent, you have to try it. I wonder why.
If you go here, it could return an error, which is concurrency unavailable.
Now, remember the scenario which I mentioned before of >> [clears throat] >> the thread pool thread pool being fully flooded with other tasks. So, let's say if you have a bunch of futures running and the whole thread pool is exhausted with other tasks.
Um it may not be possible to run your future completely concurrently.
So, in that scenario, this thing will just return an error.
versus um versus Oh, no.
versus your um io.async will just run it uh synchronously. That's kind of the major difference here.
Um it's also like a very big confusion point. Uh so, it's very important to grab this part home um before that.
Um yep.
>> [clears throat] >> And where's this thing? Yep. Okay. So, now that I have mentioned the major point, the thing is, these two both of these, they kind of just return a future.
>> [clears throat] >> Um both of these are futures.
Um but, both futures have the same characteristic. So, a future, you can you could either await it or cancel it.
Now, the thing is, at a very high level, these two things, await and cancel, are doing pretty much the the thing.
They essentially wait for uh the task to exit. But, But the only difference is the cancel thing. First of all, let me just show you the implementation first.
You know, everything should be backed by implementations.
So, if you go to cancel >> [clears throat] >> So, if you look at it, first of all, it just checks, "Hey, is the actual future complete?" If it's not complete, then it requests cancellation and then waits for it to exit. versus the await.
Await simply checks, "Hey, is a future complete?" If it's not, then it just simply waits.
So, when I say requesting cancellation, what exactly does it means? So, the thing is each of our asynchronous tasks, yeah, they have like a lot of cancellation points. So, for example, like there could be a lot of cancellation points. For example, you have function this way, it does a bunch of things.
Uh so, it's doing something and you call cancel.
So, >> [clears throat] >> at some one of the cancellation points, it would just notice the cancellation signal and return early.
And once it returns, these both of these things kind of just do some sort of clean up. Um That's kind of the major thing.
So, [clears throat] if you look at our example, we sort of create a future and we await it.
Uh we use try here because uh this thing can fail, but And note that we are also always have to cancel it because, for example, uh this thing could fail. This one is fine, but for example, if you have some other code here, some other logic or something, if this fails, if this fails and if you're not if you're not using defer error defer anything like that to cancel it, you your futures would simply get leaked.
Um so, you should always defer it and do the cancellation.
Uh it's pretty much the same uh in this thing. Uh we can also try running these.
Uh so, it should be pretty similar.
Could be experiments. Async IO guide.
Click run.
So, this is the uh async version. It's a very simple one.
>> [snorts] >> Uh it just takes a function uh that's a square.
Nothing too fancy. Now, uh this one is interesting because here we have a snooze function, and this function takes a millisecond value.
Uh let me get This function takes a millisecond value and sleeps for that that much time. So, and it's using io.concurrent.
Um and we are also keeping track of the time.
So, we start our timer, run both of these things concurrently, await them, wait for them to finish, and essentially [clears throat] see how long it took.
So, if we run this, oh, no. That wasn't good. Uh this Yep.
Uh so, if you run this, uh it says 250 millisecond task because both of these tasks uh have were given 50 in the parameters.
250 millisecond task took 50 milliseconds.
Now, this kind of proves that uh these two things were running concurrently and not, you know, >> [clears throat] >> not synchronously, which could be the case in your in your regular async one versus the concurrent version.
That's kind of the major uh chunk of what I wanted to discuss for these two.
>> [snorts] >> Uh Uh, there are a bunch of other things here as well.
So, if you go to group.
Now, this is an interesting one. Uh, if you go to group, it says Uh, essentially all the thing is one of the major use cases of the regular futures could be for example, um, >> [snorts] >> uh, let's say you have some sort group of tasks, you know, you want to do a bunch of IO operations or anything like that. You have a group of tasks and um, you kind of want to keep track of them. So, you create a bunch of tasks and you fan them out. You could do it manually with futures. Essentially, you could create a slice of futures, start them and then, uh, wait await each of them. versus this is just a nicer primitive on top of futures.
Uh, so if we go to group, in this scenario, what you can do is, um, make it a little bit bigger.
In this scenario, we have a worker.
Worker sleeps and does some sort of other operation.
>> [snorts] >> Um, it's pretty similar to them. But, we initialize it with a group.
Um, this is the result vector, uh, result slice.
Uh, it's pretty similar to them. Uh, we have the defer cancel, otherwise you would leak futures.
And, uh, instead of starting [clears throat] each of them and, you know, keeping track of those futures, you could just initialize them inside a group and then await all of them at once.
>> [clears throat] >> So, what this thing does is blocks until all of them are finished.
And, uh, the thing is if any of them error out, uh, this error defer would catch them and essentially read them all.
So, uh, we could try running this as well.
So, zig run for Yeah, [snorts] so it's the squares, um, >> [clears throat] >> we're doing a bunch of these operations.
Uh so, we're just doing the square for each of them. Right. So, next one we're looking at is select. Uh now, this one is interesting um because uh let's see what this is first. Um it says execute tasks together providing a mechanism to wait until one of the more tasks complete.
Now, this thing is basically an very lightweight wrapper, essentially a decent abstraction around groups and queues.
Um so, let me just show you first of all what this thing does.
>> [clears throat] >> So, uh as think of a certain scenario where you have a lot of tasks. So, for example, um let's say you have a bunch of URLs, uh endpoints, and you just want to quickly want to figure out which one is the closest to me.
Essentially, what you what you trying to do is like find the endpoint with the lowest latency. So, what you would do is [clears throat] you want to have uh you know, just fan out requests in parallel and figure out which one is the fastest uh in the response to you.
So, uh you could use groups for that, but you would have to build things on top of it, but this thing kind of gives you that s- out of the box. So, here we have function um it just sleeps like the rest of them for [clears throat] the certain amount of time.
Here, uh the syntax of this is pretty similar. Um so, we just initialize it with an IO and uh a slice.
And >> [clears throat] >> I'll tell you what this cancel is called is later.
Uh but, just like group, uh you can uh you can kind of fan out with this dot async syntax. [clears throat] Uh this kind of this thing and you give the function and the arguments.
And then you can just switch essentially.
What this is is you're just waiting for hey, which which one is the thing which returns first.
>> [snorts] >> And now notice that >> [clears throat] >> our work function returns uh an error or U64.
Now I ideally well, if you're doing things synchronously, you would be doing things like try work, you know, since that thing could return error. Uh so you would add a try work, catch errors or something like that.
Um but [clears throat] in this scenario um this thing just returns us this whole package.
Essentially, this this either this U64.
And we would have to try it ourselves.
So the thing is let's say we did this thing uh and we got this. Now the thing is at this point we still don't know if the function succeeded or returned an error because we just got this whole package.
So we would have to kind of unwrap it with with a try.
Um and the thing is this thing can fail because since it's a try uh uh so this is the point where this thing kind of uh comes in our interest. This just kind of it's in defer, so whenever we go out of scope, >> [snorts] >> this thing would will fire. And it's just going to clean up all the things. So uh this thing is useful uh when let's say one of the requests succeeded and we go out of scope, so it's going to cancel all of the other things and cancel all the other things and just uh collect them.
And it's also useful when uh something fails because when something fail, it would also go out of the scope and it would just reap everything.
Um >> [clears throat] >> the thing I'm super interested in is the actual implementation. So, if we go in here, you'll notice that it only uses these two abstractions.
Now, these are the two ones that we talked about before.
Uh group and queue.
So, select here we're using the um async version, but there's also a concurrent version. It's pretty similar to like I mentioned before, but if you look at it, if you just [clears throat] look at what this thing does, um so, we get in here. This thing kind of prepares a context wrapper with a function.
Uh if you look at this function, um >> [snorts] >> it takes a bunch of things. Um Here, LM is just uh wait a second. LM is kind of just it it calls the actual function that we give it.
Um and >> [clears throat] >> and puts that puts the um output of that thing into a queue.
So, this this is kind of the wrapper this thing creates.
Uh it wraps it around once again, and notice that it puts uh it kind of puts it in a group and kind of launches it.
So, if you look at this uh dynamic play dispatch call, remember this name, i.vtable.group async.
So, something like this, uh if we go to our group, if we go to async, wait, isn't this the same?
Haha.
Yeah, so uh the whole point is uh these two um these two, they kind of it's just a wrapper around uh the select one. Essentially, we just create a context >> [snorts] >> uh return the uh the function to wire a group. Uh the very first thing which finishes, uh it kind of just puts it in there.
Um >> [clears throat] >> we kind of collect it and discard everything, so the channel kind of just closes and everything else kind of gets collected.
This is kind of the overall implementation of this thing. We can also try running it.
Uh file select.
Okay. So, the this what this thing is doing is >> [snorts] >> uh we're just doing two things. Uh it just says, "Hey, which one finishes first, fast one or slow one?"
Um So, here if we just make it big, now the slow one should win.
Yeah, pretty much this.
Um >> [clears throat] >> So, we can go to the next one, uh which is Here we go here.
Uh I want to talk about a bunch of other things, for example, like cancel.
Now, you might be thinking, "Hey, what's the big deal about it about this? Why would you, you know, want to make a whole example about it because uh since we have talked about cancel in each one of these before."
Now, the thing is um the cancel is a lot is a lot more complex than, you know, one of those basic things.
Um so, [clears throat] you might think that a cancel would would only be used with uh some sort of IO operations uh and not with pure CPU operations. So, for example, if you if you uh let's say in one of these examples, uh in concurrent example, if you started a snooze function, right? And if you let's say if you weren't using uh IO in here, IO in here for instead of IO, but, you know, you you were running some sort of server, while true, accept connections, something like that, right? And if you even if you were not accepting a connection because that's some sort of IO, but if you're doing some sort of, let's say, some math, right?
Not just math, long running math.
In that scenario, um it would be [clears throat] extremely it wouldn't be possible for this thing to um to [snorts] get the cancels cancellation signal because hey, this is a pure CPU operation. You're just burning CPU cycles.
So, the thing is um they [clears throat] have given us a nice primitive for this as well.
So, there's this thing called io.checkcancel.
Uh so, if we go there and search for this.
>> [snorts] >> So, generally we don't need to support cancellation, but for CPU tasks, um uh we can this would be useful for that.
So, you might be familiar with yield.
Essentially, uh in long running CPU tasks, you occasionally yield yield for something so that uh you don't just keep burning the cycles for the for the particular task and others could have a chance. So, essentially you're willingly giving up control.
It's pretty similar to that.
Uh but the thing is it just checks cancel. So, for example, in this scenario, we have an infinite loop.
And every once in a while, so for every 50,000 iterations, um >> [clears throat] >> we just check, "Hey, is this particular task still alive or canceled?"
And if it's canceled, we just, you know, can cancel everything and return.
>> [snorts] >> Um and this one is just a regular example. So, we can we can run it.
Um 506 cancel.
Now, notice that CPU worker stopped.
And if we didn't have something like this, um in this scenario, the CPU worker never stops because we are never checking for cancel.
Uh so, yeah, this was one of the examples I really want to talk about. Um next one that we're looking at is the queue.
It's essentially just an MPMC channel.
Um many producer, many consumer.
Uh designed to perform for inter-task communication. So, uh essentially, I'm going to show you the code example.
So, you have a bunch of futures.
Uh we have two producers and two consumers.
Producers produce things to the queue.
Consumers consume things from the queue.
And >> [clears throat] >> it it's just there's nothing too special about it. It's just a regular MPMC channel. So, uh we initialize it with a buffer. So, uh whenever the buffer is full, the producers would just uh will just block until uh until it frees up.
Uh same goes for the consumer.
Whenever the buffer is empty, the consumers will just block until we have something in the buffer.
So, it's pretty straightforward. Um nothing too special about it. Um So, uh in our example, we are just whenever we consume something, we just fetch add. Uh one thing that's uh that's interesting here is the consumer API.
So, like there are a bunch of things. Um these ones, um they're mostly used for internal purposes, but the ones which I found interesting were get one.
Get one is the most basic thing.
Essentially, you just take the front thing from the queue.
And there's also this get one where you could just give min.
Essentially, like this thing blocks until I get a minimum of that many elements in my buffer and only then it returns.
It could also return early because the channel could get closed.
Um that that's one of the scenarios.
Um so it's something like that.
>> [clears throat] >> So, for example, for example, we just produce something and our consumers, whenever they consume something, we just atomically add it.
So, yeah, if we run it, um consume sum is 1090.
Um and we have a bunch of these. So, yeah, so nothing too special, just a regular mpmc channel.
Um so, if we go forward, >> [clears throat] >> the next thing that I wish we should talk about is the time.
So, the thing is the timers are also >> [snorts] >> a part of the um the new IO interface. So, similarly, um I'll just go very quickly through this one since it's not that complicated.
We've also like talked about this before in sleep. So, there's also um this timeout thing here.
This timeout as in you could create um a deadline. Essentially, like sleep till that min that much time and um >> [clears throat] >> that's it. So, in this example, we first uh sleep for 25 milliseconds and then we are doing a timeout for 50 milliseconds.
So, my goal with this example is we should be able to see that hey, this kind of thing actually uh sleeps the particular task. So, uh we are just uh seeing the time, how long it takes. So, in this scenario, it should take around like 40 ms. So, you can just run it. Uh but queue See, 40.2 ms. If you just run it a bunch of times, it's pretty similar to that.
The next thing that we'll be talking about is the batch operation.
Um this is for the other core primitives um that they provided in this release.
The core differences, um everything else that we've talked about till now, uh which is um futures, groups, et ceteras, they operate over functions.
But, batch here is a much lower-level primitive. Well, by the own definition, uh you can think of batch as lower-level concurrency mechanism at the operation layer.
Currently, it only supports these things. Uh eventually, they plan to move almost everything over to it.
Um so, let me just give you a small example.
Um >> [clears throat] >> Uh with batching, uh we can kind of add a bunch of low-level uh batch operations. So, for example, um uh we initialize uh slice like before.
And uh we add two operations. First one is file write streaming uh for So, for that particular file, we'll be writing this thing.
Um the other one is We're just We're just writing to std out. The other one is batch up second.
So, what this thing does is um we can enqueue these operations, and this thing would essentially start this operation, but await concurrent. This is going to wait until at least one of these previous operations is complete.
Once it's done, then we can handle that particular thing. So, once we are here, we are essentially know that all right, some IO operation is complete. We can, you know, uh get some sort of information, deal with it, uh update our internal state or something.
And then when we call next, uh it just keeps moving. So, essentially it's you can think of it like you have a lot of IO operations, you can sort of enqueue them. Um Uh they the background uh IO or threaded activation handles it.
Um and it just kind of lets you know that hey, something is done. So, you can um keep track of them.
Pretty much this. Um so, we can try running this as well.
Um This is this is a nine.
batch Um yep.
>> [clears throat] >> So, as you can see, the batch operations they are pretty small. So, it doesn't really matter. So, if you run like them like a multiple times, like the ordering could change because uh they are pretty similar.
Um so, yeah, the batch operations got complete because they write to STD out.
So, one batch operation completes. Um >> [clears throat] >> then we can kind of keep track of it, then the other one completes, we can see both of them.
So, yeah, like this kind of was the um much uh high-level information about this one since it's um it's work in progress. Um I'll eventually be adding like a lot more things to this, but currently this is all it supports. So, this is also why I kind of added this at the end of the video. All right, so that was it for this video. I hope you enjoyed it. And if you are interested in low-level systems engineering content, consider subscribing.
And uh if you want to watch another video where I kind of build something with the async IO, uh here's another one uh where I built a file downloader with the new async IO. I hope you enjoy it.
Yep, see you in the next one.
Related Videos
LBF101 Creating an XML Changelog
liquibase7511
3K views•2026-06-15
Alta Labs Cloud Dashboard Real time Network & Xnet Insights!
ShinyTechThings
158 views•2026-06-17
Wait... Group Policy Not Applying? Check This First!
keeplearning_iT
144 views•2026-06-15
Leetcode Weekly Contest 506 | Life's boring these days
Pudeesht
2K views•2026-06-14
microJAM: MAKING A MICRO GAME FOR A GAME JAM IN CLOJURESCRIPT AND TOTALLY NOT C
janetacarr
156 views•2026-06-18
Partitioning vs Bucketing vs Clustering: How to Make Queries 100x Faster
thedataandaiguy
194 views•2026-06-16
Design Claude Code Like a Senior Engineer
hayk.simonyan
344 views•2026-06-19
Linus Torvalds: AI Won’t Replace Understanding Code
SavvyNik
140 views•2026-06-19











