Posted on ::

I'm building the same encrypted key-value store in Rust and Zig simultaneously. 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, I have an intermediary verdict: I don't want to go back to Zig as much as I want to Rust.

This surprised me. I went in with the exact opposite expectation.

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)

Zig experience

The code itself was straightforward, a simple wrapper around std.StringHashMap. But the REAL challenge was understanding memory management, internal hashmap implementation, memory bugs, testing memory leaks, etc (more about this in the zig hashmap gotcha and stack vs heap memory)

Programming concepts from the language were easy but the system knowledge required was harder than I thought.

I spent more time learning ABOUT memory than writing code. Don't get me wrong, this is an amazing thing, and really speaks volumes of Zig! The language got out of my way immediately, but I had to deeply understand what was happening underneath.

Rust experience

The opposite problem. The code took longer to write because I had to learn String vs &str and some language specifics like mutable, option, result, etc.

I didn't think that much about memory management because I could rely on the compiler shouting at me. It also probably matters that I just built this in Zig, meaning I had to manage memory manually and suffered some use-after-free bugs. The programming concepts were a bit more complex but not really that bad.

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

Module 2: Wire protocol (parser/serializer)

Rust

Rust enums are incredibly powerful, each variant can hold different data types <3. Combined with pattern matching, parsing commands was clean and type-safe.

Result<T, E> and Option<T> for explicit error handling are really nice. Everyone should do this. Why does not every language do this.

Module 2 wasn't as hard as I expected. Building the parser was straightforward with Rust's type system.

Zig

Zig's tagged unions work similarly to Rust's enums with data, and I was able to build a parser that converts text commands into typed ParsedCommand union variants. The syntax was different but the concept clicked.

Then I discovered a memory leak pattern. Some responses returned string literals like "OK", while error responses used allocPrint() which allocates memory. The caller didn't know which responses needed freeing. Ended up passing a fixed-size buffer to avoid allocation (which is 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, the maturity gap

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

Zig

I spent an hour fighting the package system trying to install clap for CLI argument parsing. Eventually gave up and wrote manual arg parsing instead.

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. TCP worked eventually, but even basic things like printing to console required workarounds. I spent 6-8 hours on this module, mostly fighting tooling instead of writing code.

Rust

Everything just worked. TcpListener::bind() and .accept() did what they said on the tin.

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.

Cargo dependency management is 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 in a single thread.

The hardest part was understanding the registration concept. You register the TCP listener with a token, and when a client connects, you register that connection with a unique token. You keep a HashMap of Token -> TcpStream, so when poll notifies you "Token 5 has data ready", you look it up and handle it. Once that clicked, implementation was straightforward.

mio wraps epoll (Linux) and kqueue (macOS), so I didn't have to deal with platform-specific syscalls. Clean API: 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. Each module feels like forward momentum. I learn something, implement it, it compiles, it runs. Done.

In Zig, every task feels like fighting. Either 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. I Matklad said that is not the immaturity, but rather "unfinshness".

Both languages teach systems programming, just different aspects. Zig forces you to understand memory management deeply while rust forces you to understand ownership and borrowing deeply.

My verdict

I came into this expecting Rust to be harder than Zig (which in a way is true). But Zig's immaturity is more painful than Rust's complexity of ownership, borrowing, and lifetimes.

I'd rather fight with a strict compiler than fight with missing documentation.

What now?

The comparison already worked. Doing both simultaneously gave me a fair comparison. And my unexpected take is clear: ecosystem maturity matters more than I expected.

I will 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 is still a space in my heart for Zig, and I think will give it another serious try when they re-release async await. It looks amazing.


Note: Writing these types of posts is really not hard when you have proper logs, more about that in my learning framework.

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

Table of Contents