Two compiler teams rewrote in opposite languages. The numbers are in.
Last week the Roc programming language team announced they had reached feature parity after rewriting their compiler from Rust to Zig. The rewrite took 487 days and covered roughly 300,000 lines of code. Around the same time, the Bun JavaScript runtime team published their own experience report going the opposite direction: 500,000 lines of Zig rewritten into Rust in 11 days.
Two serious compiler projects, big codebases, opposite language moves. I have been waiting for a pair of writeups like this because compiler rewrites usually happen in private and the lessons stay internal. These two are public, detailed, and specific enough to argue with. So let me argue with them.
The 487-day vs 11-day gap, and why it is not about the languages
487 days versus 11 days. That looks like a damning comparison for Zig or for the Roc team's process, but it is not. The reasons the two rewrite timelines differ have almost nothing to do with Rust versus Zig.
Bun did a direct port. Their goal was to reproduce the same behavior in a new language, and they apparently used AI-assisted tools to accelerate the mechanical translation. Roc's rewrite was not a port. They rewrote because they wanted to change the compiler's architecture fundamentally: a new lambda set resolution system, a new parser, a new type checker, and a switch to zero-parse deserialization for their caching layer. By the time they decided to rewrite, several contributors were already planning to rewrite major chunks of the compiler for unrelated reasons. They were going to rewrite most of it anyway, so doing it all at once made more sense than the Ship of Theseus approach.
In other words, Bun translated code. Roc redesigned a compiler and happened to use a different language while doing it. Comparing the two timelines tells you about project scope, not about Rust or Zig. The Roc post is explicit about this and I respect the honesty. A less careful author would have buried the caveat.
Build times: the one area where the numbers are unambiguous
Here is where the data gets interesting. The Roc team measured build times on the same Intel desktop running Ubuntu 26, comparing their old Rust compiler at two different Rust versions against the new Zig compiler at two different Zig versions. These are their numbers, measured cold (no cache) and incremental (trivial edit to the parser):
Rust 1.85 was current when the rewrite started, 1.97 is current now. The Rust tooling team shaved incremental builds from 10s to 3.4s in 18 months. That is a big jump.
35 milliseconds. On a codebase 50% larger than the Rust one that takes 3.4 seconds. The catch: this requires the nightly 0.17 prerelease with an incremental compilation bugfix that stable 0.16 does not have yet.
Let me be fair to Rust first. The Rust compiler team improved incremental build times by 66% over 18 months, from 10 seconds to 3.4 seconds, without the Roc team changing anything. That is a real improvement and the Rust contributors deserve credit for it. If you are on a Rust project and your builds feel slow, upgrading your toolchain may buy you more than you expect.
But Zig's 35 milliseconds is not in the same category. It is not "faster." It is a different kind of fast. The Roc post calls this out and a Zig team member confirmed in the HN thread that the rebuild itself takes maybe 5ms of the 35ms, with the rest spent on change detection. You are paying less time to rebuild the changed function than you would spend blinking. The closest Rust has is the experimental cranelift backend and some caching work, but there is no Rust roadmap initiative comparable to Zig's -fincremental right now.
One caveat the post flags: -fincremental currently only works on x86-64, not ARM. If your developers are on Apple Silicon laptops, they do not get the 35ms number. The Zig team member in the HN thread said they expect the ARM number to be nearly identical because code generation is a small slice of the total rebuild time, but that is a prediction, not a measured result. I would hold judgment until someone publishes an ARM benchmark.
Memory safety: where the argument gets uncomfortable
This is the part of the debate that usually descends into religion. The Roc post tries to ground it in actual bug counts, and the data is messier than either side of the Rust-vs-Zig internet argument wants to admit.
First, some context. The Roc team was using unsafe in their Rust compiler a lot. About 1,200 uses across 300K lines, compared to about 40,000 uses in rustc's 3.5M lines. For a compiler that emits and executes machine code, memory-unsafe operations are part of the job. That does not mean the borrow checker is useless, but it does mean the "Rust is safe, Zig is not" framing does not quite fit this project.
Here is the memory corruption bug breakdown from Roc's issue tracker, which the post says was classified by Claude Opus 4.8:
The Rust version had more bugs overall, but it also ran longer. The interesting row is the corruption count, not the total.
The headline "Rust had 21, Zig had 10" sounds damning for Rust. But here is the catch: none of those 21 Rust corruption bugs were in the compiler's own logic. They were all miscompilations, meaning the compiler generated correct-looking code that did the wrong thing when executed. The borrow checker does not catch that. No language-level safety tool catches that. A miscompilation is a logic bug that happens to corrupt memory at runtime.
Of the 10 Zig corruption bugs, 8 were also miscompilations. The remaining 2 were genuine use-after-free bugs in the compiler itself. Both were in error reporting: filenames in error messages rendered as garbage (the U+FFFD replacement character). The borrow checker would have caught both. The practical impact was that some error messages showed question-mark characters instead of filenames.
The post then runs a counterfactual: what if they had used Rust for the rewrite, or Zig with ReleaseSafe (which keeps runtime memory checks in production)?
- Zig with ReleaseFast (what they chose): 2 bug reports, both cosmetic errors about filenames not rendering.
- Zig with ReleaseSafe: 2 bug reports, but the errors would panic and fail to render entirely instead of rendering with a bad filename.
- Rust with borrow checker: neither bug would have existed.
After 18 months, hundreds of bug reports, and hundreds of thousands of lines of code, the conclusion is that choosing a different row in that table would have made no appreciable difference to the project. Two cosmetic filename bugs. That is the entire cost of not having the borrow checker on this particular codebase.
I want to push back on this a little, because I think the framing is convenient. The Roc compiler uses arena allocation with straightforward lifetimes and almost no interaction with a tracing garbage collector. That is exactly the kind of codebase where Zig's ReleaseFast does well. Bun's situation is the opposite. They interface with JavaScript's garbage collector, which means every allocation has to be carefully classified as GC-managed or manually-managed, and getting that wrong produces use-after-free, double-free, and leaks. Bun explicitly cited this as their reason for moving to Rust: Drop guarantees cleanup runs exactly once, and the borrow checker catches mistakes in the GC-managed versus manual boundary.
So the honest takeaway is not "Zig is as safe as Rust." It is that memory safety outcomes depend on what your code actually does. Arena-allocated compiler internals with no GC interaction? Zig's ReleaseFast plus debug-mode ReleaseSafe checks will get you most of the way there. A JavaScript runtime interfacing with a tracing collector? Rust's borrow checker and Drop semantics are doing real work that Zig's tools do not replicate. Different projects, different needs. The Roc post says this. The Bun post says this. The internet comment sections do not say this.
Why allocator ergonomics mattered more than borrow checking here
The Roc compiler uses multiple custom allocators: arenas for each compilation phase, struct-of-arrays layouts, and indices instead of pointers throughout. This is a common pattern in high-performance compilers. The Zig compiler itself uses the same approach, which is part of why Roc could reuse Zig's handwritten LLVM bitcode serializer.
Rust's ecosystem assumes a single global allocator. Off-the-shelf Rust crates almost always assume you are using the standard allocator and that Drop handles deallocation. If you want to pass a specific arena allocator into a third-party data structure, you are fighting the ecosystem. There is a long-awaited Allocator trait that is close to stabilizing, but it has been close for years.
Zig's whole ecosystem is built around passing allocators as parameters. Every standard library data structure takes an allocator argument. Struct-of-arrays support is built in. If your project is structured around fine-grained memory control, this is not a minor ergonomic difference. It is the difference between the ecosystem working with you and the ecosystem working against you.
There is also the zero-parse deserialization trick. Roc's new compiler caches its internal data structures to disk by writing them in the same layout they use in memory (indices instead of pointers, struct-of-arrays). When you run roc check a second time on unchanged source files, it loads those bytes directly into memory, does a few relocations, and starts using them. No parsing. The cache loads at memcpy speed if the bytes are in the OS disk cache. This only works because the entire compiler is built around index-based data structures instead of pointers, which is the pattern Zig's ecosystem encourages and Rust's ecosystem makes harder than it should be.
What Bun got from Rust that Zig was not giving them
Going the other direction, Bun found that Rust's implicit Drop trait was solving a real problem for them. When you mix JavaScript's garbage collector with manually-managed memory, you need to guarantee that cleanup code runs exactly once, at the right time, even when JavaScript exceptions unwind the stack. Drop does this automatically. Zig prefers explicit defer, which is more predictable but requires the developer to remember every cleanup site and get it right.
Bun's post is blunt about the cost of getting this wrong in Zig: "For Bun, correctly handling the lifetimes of garbage-collected values and manually-managed values has been a major source of stability issues, most often small memory leaks and occasionally crashes." They are not saying Zig is a bad language. They are saying that for the specific problem of interfacing with a tracing GC, Rust's implicit destructors catch a class of bug that Zig's explicit defer leaves to the developer.
Rust's ecosystem is also bigger. For most projects this matters, but for compiler work specifically, both teams report that the amount of relevant off-the-shelf code is small in either ecosystem. Roc found that most of the reusable code they wanted, like a handwritten LLVM bitcode serializer decoupled from LLVM's C++ library, existed in Zig's compiler but not in any Rust crate. Bun's needs were different and Rust's ecosystem served them better.
Things that surprised me
A few details from the two posts that stuck with me:
- The Roc team ended up with about 1,200 uses of
unsafein 300K lines of Rust. That is roughly 0.4% of lines. For comparison, rustc itself has about 40,000 uses across 3.5M lines, which is about 1.1%. The idea that compiler projects need lessunsafethan the broader Rust ecosystem average did not hold here. - Zig's
ReleaseSafecatches use-after-free at runtime by panicking when freed memory is accessed. It is less thorough than the borrow checker, has a runtime cost, and can miss things the borrow checker catches. But TigerBeetle, a Zig database that runs inReleaseFastfor production, went through a careful Jepsen test that found only two safety bugs, neither related to memory safety. Ghostty, a terminal emulator built in Zig, has had no memory-safety CVEs. These are not toy projects. - Rust still had memory-unsafety CVEs slip through in real projects. The post cites Deno (an out-of-bounds read and a use-after-free, both involving unsafe Rust), Rocket (a use-after-free CVE), and Actix (multiple memory-unsafety CVEs from a period of high
unsafeusage). "Use Rust and you will not have memory safety bugs" is not what the data says. "Use Rust and you will have fewer memory safety bugs than C or C++ on average" is closer to the truth. - Roc missed some things from Rust: private struct fields, automatic allocation in tests, the borrow checker isolating unsafe code, trivial upgrades between versions, dead code detection. None of these are showstoppers, but they are real friction points. Zig is pre-1.0 and makes breaking changes on minor releases. The Roc team accepted this, but if your organization cannot absorb periodic breaking-change upgrades, that is a cost to weigh.
The thing that is hard to argue with
Both teams made their language choice based on what their specific codebase needed, not on which language wins internet arguments. Roc needed fine-grained allocator control, fast incremental builds, and a compiler ecosystem that matched their data structure patterns. Bun needed guaranteed cleanup semantics for GC interop and a larger ecosystem. Both got what they needed. Neither post claims their choice generalizes to all projects. The comment sections of both posts are full of people claiming it does anyway.
Where I land
If you are building a compiler, a database, or any system where allocator control and build speed dominate your developer experience, the Roc data is hard to ignore. 35ms incremental builds on a 464K-line codebase is a number that changes how you work. If you have been telling yourself that slow Rust builds are just the cost of safety, the Zig 0.17 numbers challenge that assumption directly.
If you are building a system that has to interface with a garbage-collected runtime, a JavaScript engine, or anything with complex lifetime semantics that cross FFI boundaries, Bun's experience suggests Rust's Drop and borrow checker are doing real work that Zig's tools do not replace.
The thing I keep coming back to is that both of these projects succeeded. Roc has a working compiler with better build times and a clean memory-safety record in practice. Bun shipped their rewrite and reports better stability. The disagreement is not about which language is better. It is about which tradeoffs fit which problems, and that is a question you can only answer by looking at what your code actually does.
If there is a lesson here, it is that experience reports with real numbers matter more than language advocacy. Two compiler teams publicly sharing their bug counts, build times, and architectural reasoning is worth more than a thousand "Rust vs Zig" tweets. I would like to see more of this.
The full Roc post and the Bun post are both worth reading in full. The HN discussion on the Roc post is unusually good for a language-war thread, with a Zig team member, compiler engineers, and Richard Feldman himself engaging substantively on the memory safety framing.