buenomini

Rust vs Zig: the ecosystem maturity gap

I expected Zig to be the simple one and Rust the hard one, then I built the same project in both and Zig's tooling fought me at every step.

6 min readrust, zig, learningrkv

I'm building the same encrypted key-value store in Rust and Zig at the same time, module by module, feature by feature. The goal was a fair comparison, and to learn both languages by solving identical problems in parallel.

Four modules in, my intermediate verdict is that I don't want to go back to Zig as much as I want to go back to Rust. Which surprised me, I went in expecting the exact opposite.

# What I expected

Zig: simple language, closer to C, full control. Manual memory management means you learn how things really work.

Rust: complex language, steep learning curve. Ownership, borrowing, lifetimes. Everyone complains about the borrow checker. Lots of syntax to learn.

I thought Zig's simplicity would make it easier to be productive. I was wrong.

# Module 1: storage layer (HashMap wrapper)

In Zig the code itself was straightforward, a simple wrapper around std.StringHashMap. The REAL challenge was understanding memory management, the hashmap internals, memory bugs, testing for memory leaks, etc. (more on this in the Zig hashmap gotcha and stack vs heap memory). The language concepts were easy, but the systems knowledge it required was harder than I thought, and I spent more time learning ABOUT memory than writing code. Don't get me wrong, that's an amazing thing and really speaks volumes about Zig! The language got out of my way immediately, I just had to deeply understand what was happening underneath.

Rust was the opposite problem. The code took longer to write because I had to learn String vs &str and language specifics like mutability, Option, Result, etc. I didn't think that much about memory management because I could rely on the compiler shouting at me (it probably also matters that I had just built this in Zig, where I managed memory by hand and suffered some use-after-free bugs). The concepts were a bit more complex, but not really that bad.

No allocators, no manual freeing, no use-after-free possible. I fought the type system instead, but once it compiled, it was ok.

# Module 2: wire protocol (parser/serializer)

Rust enums are amazing, each variant can hold different data types <3. Combined with pattern matching, parsing commands was clean and type-safe. And Result<T, E> and Option<T> for explicit error handling are so nice. Everyone should do this. Why doesn't every language do this? Module 2 wasn't as hard as I expected, Rust's type system made the parser straightforward.

Zig's tagged unions work a lot like Rust's enums with data, and I built a parser that turns text commands into typed ParsedCommand union variants. Different syntax, but the concept clicked.

Then I found a memory leak pattern. Some responses returned string literals like "OK", while error responses used allocPrint(), which allocates, and the caller didn't know which responses needed freeing. I ended up passing a fixed-size buffer to avoid the allocation (a waste of resources for simple error strings, but at least it's consistent). In Rust I didn't have to think about any of this, the type system handles it.

# Module 3: TCP server, where the gap showed up

So far it was ok, I liked some things from each and I mostly liked both. But this is where that changed.

In Zig I spent an hour fighting the package system trying to install clap for CLI argument parsing. Eventually I gave up and wrote the arg parsing by hand.

Then I needed to read user input from stdin, but std.io.getStdIn() doesn't exist in my version of Zig. I had to dig through the actual library code to figure out it's std.fs.File.stdin() now. A lot of guesswork involved. APIs keep changing between versions with no migration guide, and TCP worked eventually, but even basic things like printing to the console needed workarounds. I spent 6-8 hours on this module, mostly fighting tooling instead of writing code.

In Rust everything just worked. TcpListener::bind() and .accept() did what they said on the tin. The documentation was clear and accurate. Need to print something? println!("Connected to {}:{}", host, port). Done. Buffers were straightforward: let mut buffer: [u8; 1024] = [0; 1024];. Error handling with the ? operator makes propagation trivial, and Cargo makes dependency management painless.

I spent 2-3 hours on this module, actually implementing features.

# Module 4: event loop

I built an I/O multiplexing event loop with mio in Rust. The server now handles multiple concurrent connections on a single thread.

The hardest part was understanding registration. You register the TCP listener with a token, and when a client connects you register that connection with its own unique token. You keep a HashMap of Token -> TcpStream, so when poll tells you "Token 5 has data ready", you look it up and handle it. Once that clicked the implementation was straightforward. mio wraps epoll (Linux) and kqueue (macOS), so I didn't have to deal with platform-specific syscalls, and the API is clean: register sockets, poll for events, dispatch to handlers.

I haven't implemented this in Zig yet. And I'm kinda scared.

# Honestly...

I've been avoiding it, but I need to admit it: switching back to zkv to implement the event loop in Zig feels like a chore. I don't want to do it.

In Rust I feel productive. Things work, I make progress. I learn something, implement it, it compiles, it runs. In Zig every task feels like a fight, either with the package manager, missing documentation or APIs that changed between versions. It's quite some work (for me) to figure out basic things that should be simple. Matklad has a word for this, "unfinishedness", and his point is that it's a different thing from immaturity.

Both languages teach systems programming, just different parts of it. Zig makes you understand memory management deeply, Rust makes you understand ownership and borrowing deeply.

I came in expecting Rust to be harder than Zig (which in a way is true). But Zig's immaturity hurts more than Rust's ownership, borrowing and lifetimes. I'd rather fight a strict compiler than missing documentation.

# What now?

Doing both at once gave me the fair comparison I wanted. What I didn't expect is how much ecosystem maturity matters.

I'll finish rkv completely in Rust first, then decide if I want to go back to zkv. Or I might just accept that this became a Rust learning project, and that's okay. There's still a space in my heart for Zig, and I think I'll give it another serious try when they re-release async await. It looks amazing.


Note: Writing posts like this is pretty easy when you keep proper logs, more on that in my learning framework.

Note: This is based on Zig 0.15.2 (pre-1.0, constantly changing). Zig will mature, and the language itself is interesting. But right now, in 2025, Rust's ecosystem is years ahead.