I've shipped production Rust for years, loved the borrow checker's iron grip on memory safety, the zero-cost abstractions, the fearless concurrency.
But a few months back I hit a wall on a side project: a small bytecode VM with mark-sweep GC (inspired by Crafting Interpreters). The safe Rust version fought me every step; the "unsafe" rewrite was a nightmare of raw pointers, Miri violations, and index gymnastics just to keep the checker happy. Out of curiosity I ported the whole thing to Zig in one weekend.
The result shocked me: the Zig version was safer, noticeably faster, and dramatically more pleasant to write.
When "Unsafe Rust" Stops Being Safe
Rust's safety guarantee evaporates the moment you drop into unsafe. Suddenly you're juggling aliasing rules, provenance, and UB landmines that even Miri can't always catch quickly.
In my VM, every stack frame, upvalue, and instruction pointer had to live behind indices instead of raw pointers, because turning mut T into &mut T and back invited instant UB if anything aliased.
Zig actually does something interesting. It treats low-level code as the default and gives you first-class tools for it:
- Pointers are non-null by default (opt-in ? for nullable).
- Slices carry length and are bounds-checked in debug/safe builds.
- Zig's GeneralPurposeAllocator detects use-after-free, double-frees, and leaks with full stack traces, no external sanitizer required.
- No hidden aliasing UB; pointers just… work the way you expect.
The same VM in Zig let me use direct pointers everywhere (stack_top, frame, upvalues) without fear.
The code stayed explicit but never felt fragile. Roc-lang's team hit the same wall: their stdlib was drowning in unsafe Rust that "got in the way."
They rewrote chunks in Zig and never looked back, Zig's leak detection in tests alone was a game-changer.
Both Compile-Time and Runtime
Zig's compiler is fast. My full VM + tests rebuilt in <200 ms; the Rust version took seconds even with incremental. That feedback loop alone makes you write safer code, you test and fix faster.
Runtime numbers were even more surprising. On the same benchmarks (fibonacci stress, method calls, string equality):
- Zig beat the Rust version by 1.54–1.76× across the board.
Why? Direct pointer arithmetic and zero-indirection stack ops. Rust's index-based workaround killed cache locality.
Real-world projects show the same pattern. TigerBeetle (a distributed financial database) chose Zig precisely for predictable latency and explicit memory control.
They run in -DReleaseSafe mode (spatial safety + overflow checks) and hit microsecond-level performance that would have required heroic unsafe Rust contortions.
So When Should a Rust Engineer Reach for Zig?
- Heavy manual memory management (VMs, allocators, parsers, high-frequency trading cores)
- When >30 % of your Rust crate would be unsafe anyway
- When compile speed or raw pointer ergonomics matter more than "safe by default" for the whole codebase
Rust remains my daily driver for almost everything else, web services, CLIs, embedded, kernels.
Its ecosystem and guarantees are unmatched. But the moment a hot path demands total control and the borrow checker becomes an obstacle instead of a friend, Zig doesn't just compete, it wins on safety and speed.
If you've ever cursed at Miri or spent hours turning pointers into indices "just to make it compile," do yourself a favor: port one small unsafe-heavy module to Zig this weekend. You'll feel the difference immediately.
Both languages are incredible. The trick, as always, is knowing when each is the right tool.
More reads: