My journey with code. Today threads: Threaded vs Evented

Published: (February 24, 2026 at 05:41 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Why Zig?

I heard about Zig for the first time because of Bun JS. They said it was the fastest runtime in the market. At first, I didn’t pay much attention – not my level. But it kept appearing everywhere with the best performance. Why so fast? They said it was written in Zig.

That name caught my curiosity. I had to test Zig.

Now I’m here, fully into Zig. I’m building my own game engine, my own web framework. But Zig changes a lot, and that’s confusing. I use the master dev version to access the newest features, but I don’t recommend that!

The new std.Io is very different. That’s why I started my tests.

The Problem: Threaded vs Evented

In the new Zig std.Io, we have two main ways to handle concurrency:

  • std.Io.Threaded – uses a thread pool. Good for CPU‑bound tasks (calculations, physics, particles).
  • std.Io.Evented – uses io_uring on Linux. Good for I/O‑bound tasks (network, files, databases).

But which one should I use? I needed to test it myself.

My Tests

I created several benchmarks to understand the difference. All code is available on my GitHub.

Test 1: CPU‑Bound – 1 Million Particles

I simulated 1 million particles with 10 iterations:

// particle_benchmark.zig
fn computeAllParticles(particles: []Particle, dt: f64) void {
    for (particles) |*p| {
        p.x += p.vx * dt;
        p.y += p.vy * dt;
        p.vx *= 0.999;
        p.vy *= 0.999;
    }
}

Results

TypeTime
Single‑threaded43 ms
Threaded (4 threads)30 ms (1.4× faster)
Evented104 ms (slower)

For CPU‑bound work, Threaded is the choice.

Test 2: Network I/O – 100 HTTP Requests

Simulated 100 HTTP requests (5 ms delay each):

// network_benchmark.zig
fn simulateNetworkRequest(id: usize) TaskResult {
    var ts: std.posix.timespec = .{ .sec = 0, .nsec = 5_000_000 };
    _ = std.posix.system.nanosleep(&ts, &ts);
    return .{ .id = id, .status = "ok" };
}

Results

TypeTime
Sequential503 ms
Concurrent (Evented + Group)261 ms

For I/O‑bound work, Evented with Group is better.

Test 3: Memory Usage

Both approaches use roughly the same memory (~38 MB for 1 million particles). The only difference is the thread‑stack overhead for the threaded version.

What I Learned

  • CPU‑bound tasks → Use Threaded
  • I/O‑bound tasks → Use Evented
  • The same code works with both; just change the I/O implementation.

“Does my task wait for I/O? Use Evented. Does my task do CPU work? Use Threaded.”

API Quick Reference

Threaded (for CPU‑bound)

var threaded = std.Io.Threaded.init(allocator, .{
    .async_limit = std.Io.Limit.limited(4),
});
const io = threaded.io();
defer threaded.deinit();

var group: std.Io.Group = .init;
group.async(io, myFunction, .{args});
group.await(io) catch {};

Evented (for I/O‑bound)

var evented: std.Io.Evented = undefined;
try evented.init(allocator, .{ .thread_limit = 4 });
const io = evented.io();
defer evented.deinit();

var group: std.Io.Group = .init;
group.async(io, myFunction, .{args});
group.await(io) catch {};

Sources

Code Repository

All benchmark code is available on GitHub:

This article is a personal record of my learning journey with Zig and low‑level programming. It’s not a tutorial – it’s my chalkboard.

0 views
Back to Blog

Related posts

Read more »