No Way Back to Zig
I used to love Zig. I mean genuinely love it. I watched Primeagen gush about it, I read the “Zig is the better C” posts, I wrote…
No Way Back to Zig

I used to love Zig. I mean genuinely love it. I watched Primeagen gush about it, I read the “Zig is the better C” posts, I wrote allocator-passing code and felt like a systems programming monk. Explicit everything. No hidden control flow. Comptime instead of macros. It felt honest.
Then I shipped Rust in production. And I never went back.
This week made it painfully clear why, because the Zig vs Rust debate just got its final, brutal, real-world data point: Bun, the largest Zig codebase on the planet, is now a Rust codebase. And the Zig creator’s response to that migration told me more about Zig’s future than any benchmark ever could.
What actually happened
On July 9, 2026, Andrew Kelley published “My Thoughts on the Bun Rust Rewrite.” In it, he says the Zig core team regularly read Bun’s source code and became “horrified” at the programming practices they found. He describes Bun as a net liability for the Zig Software Foundation, notes the $60K/year donation from Bun’s company quietly stopped after the Anthropic acquisition, and frames Jarred Sumner’s whole journey as “beginner energy” that never matured.
Read that again. The flagship project of your language, the single biggest reason anyone outside your community has heard of Zig, ships half a million lines of your language for five years, and your public takeaway is: they wrote it wrong.
In the artisanal, hand-crafted era of programming, this would be a classic flex from the top of the contempt chain. Language author dunks on user. Crowd nods. But in 2026 it reads completely differently. It reads like a confession.
Because here is the thing: if the most funded, most motivated, most visible Zig team in existence produced code that horrifies you, that is not a Bun problem. That is a Zig problem. A language whose correct usage requires an elite priesthood and a style guide enforced by vibes does not scale. Kelley’s own answer to why Bun’s code was bad is essentially “skill issue.” Rust’s answer to the same class of bugs is “compiler error.” One of these answers works at a million lines. The other one is a eulogy.
The numbers do not care about your feelings
Look at what Bun was actually dealing with. 535,496 lines of Zig, excluding comments, in effectively one giant compilation unit. A bug tracker where a huge percentage of crashes were use-after-free, double-free, and forgotten frees on error paths. Jarred said it plainly: he was tired of burning his life fixing memory leaks and crashes.
These are not exotic bugs. These are the same bugs that have haunted C and C++ for fifty years. Zig makes them more ergonomic to write and slightly easier to spot, but it does not make them impossible. In safe Rust, that entire bug class is a compile-time rejection. Use-after-free is not a 3 AM pager alert, it is a red squiggle before you even commit.
We have seen this movie before. Google’s Android team rewrote Binder, the IPC backbone of every Android phone, from C to Rust. Not because Rust was trendy, but because after more than a decade of hardening, the bug discovery rate in the C implementation still outpaced the team’s ability to fix things. If hand-crafted, battle-tested, Google-reviewed C could not hold the line after fifteen years, what exactly is a style guide going to do for a startup shipping a JavaScript runtime at breakneck speed?
Android’s broader data is the killer stat: after Google started writing new native code in Rust, the share of memory safety vulnerabilities in Android collapsed from around 76% of all severe vulns to well under a quarter. That is not marketing. That is the compiler doing the work that code review was always pretending to do.
Show me the code, or it did not happen
Let me be precise about the claim, because Zig fans will rightly call out anything sloppy here. Zig is not slow. Per request, a well-written Zig server and a well-written Rust server are within noise of each other. Both compile to the same LLVM soup. The difference is not throughput. The difference is what happens to your rare bugs when you multiply them by millions of requests per second.
Here is the math that decides everything. Say your server has a lifetime bug that fires once in ten million requests, some race between an async socket write and a buffer free that only loses when the event loop is under just the right pressure. In dev, you will literally never see it. In CI, never. At Bun’s scale, at one million requests per second, that bug fires every ten seconds. Your “one in ten million” edge case is now a pager alert on a loop. Scale is a bug amplifier, and Bun’s own bug tracker was the receipt: the dominant crash categories were use-after-free, double-free, and forgotten frees on error paths.
So let us write the three bugs that were killing Bun, in both languages, and see who catches them.
Bug 1: the error-path leak. The single most common leak pattern in manual memory management. You allocate, a later step fails, the early return skips your cleanup.
fn handleRequest(alloc: Allocator, req: *Request) ![]u8 {
. const headers = try alloc.alloc(u8, 4096);
. // the fix is one errdefer here. you have to remember it.
. // in every function. forever.
. const body = try readBody(alloc, req); // fails? headers leaks
. defer alloc.free(body);
. return try render(alloc, headers, body);
}
This compiles clean. It passes tests, because tests rarely exercise failure paths under memory pressure. In production, every malformed request leaks 4KB. At a few thousand bad requests per second, that is your RSS graph turning into a hockey stick and your node getting OOM-killed at 4 AM. Zig’s answer is errdefer, which is a genuinely nice feature, but it is opt-in vigilance. The compiler does not care if you forget it.
fn handle_request(req: &mut Request) -> Result<Vec<u8>> {
. let headers = vec![0u8; 4096];
. let body = read_body(req)?; // early return? headers is dropped. always.
. render(&headers, &body)
}
In Rust there is nothing to remember. Drop runs on every exit path, success or failure, because ownership is a language rule, not a convention. This bug category does not exist.
Bug 2: the async use-after-free. This is the one that matters for a runtime like Bun, because everything is an event loop and every buffer lives across a callback boundary.
fn onRequest(server: *Server, req: *Request) void {
. const resp = server.alloc.create(Response) catch return;
. resp.body = buildBody(req);
. req.socket.write(resp.body, onWriteDone); // async: queues the write
. server.alloc.destroy(resp); // "cleanup". body now dangles in the queue
}
Compiles. And here is the evil part: it usually works. When the socket buffer has room, the write completes synchronously before the memory gets reused, and everything looks fine. It only detonates when the kernel buffer is full, which is exactly what happens at millions of requests per second under real network backpressure. So the bug is invisible at low load and constant at high load. This precise shape, freeing a buffer that an in-flight async operation still references, is the canonical JS-runtime crash, and it is why Jarred spent years of his life in ASAN traces.
async fn on_request(server: &Server, mut socket: Socket, req: Request) {
. let resp = Response { body: build_body(&req) };
. tokio::spawn(async move {
. socket.write_all(&resp.body).await; // resp is owned by the task
. }); // resp is freed here, after the write completes. guaranteed.
}
Ownership moves into the task, so the buffer cannot be freed while the write is pending. And if you try to write the Zig version in Rust, borrow a buffer, hand it to something async, then free it early, you do not get a p99 crash. You get error[E0505]: cannot move out of resp because it is borrowed before the code ever runs. The compiler just told you about a production incident six months in advance, for free.
Bug 3: the cross-thread data race. Bun runs work across threads. Share a hot cache the naive way in Zig:
var cache = std.StringHashMap([]u8).init(alloc);
// worker threads all call this. no lock. compiles fine.
fn getCached(key: []const u8) ?[]u8 {
. return cache.get(key); // concurrent get + put = corrupted hashmap
}
Zig will compile this without a whisper. The hashmap rehashes on one thread while another reads it, and you get a corrupted bucket, a wild pointer, and a crash dump that points nowhere near the actual bug. These are the weeks-long heisenbugs.
static CACHE: LazyLock<Mutex<HashMap<String, Vec<u8>>>> = …;
// try to share a non-thread-safe type across threads instead and you get:
// error[E0277]: `Rc<RefCell<HashMap<…>>>` cannot be sent between threads safely
Rust makes unsynchronized sharing a type error via Send and Sync. Data races in safe Rust are not rare, they are impossible. You cannot write the bug.
Three bugs. Three compiles-clean-in-Zig. Three compile-errors-or-impossible in Rust. Now recall the Bun blog’s own accounting: a large share of their historical crash backlog was exactly these three shapes. Multiply each by a million requests per second and you understand why a founder who is anything but a Rust ideologue, a guy who chose Zig and donated $60K a year to it, looked at his bug tracker and gave up on discipline as a strategy. Discipline does not scale. Type systems do.
Zig has a lane, and it is not Bun’s lane
I want to be fair here, because I did love this language. Zig is genuinely excellent in one specific configuration: a small, senior, obsessive team with total discipline and a bounded domain.
TigerBeetle is the proof. A financial database written in Zig by a small crew of exceptional engineers, with static allocation, no dependencies, deterministic simulation testing, and a NASA-grade style guide called Tiger Style. It works beautifully. It works because the team is tiny, the domain is frozen, and every line gets adult supervision.
Bun is the opposite of that in every dimension. Massive surface area (runtime, bundler, test runner, package manager, shell), multi-language stack with JavaScriptCore’s C++ underneath, brutal shipping velocity, and a team culture built on iterating fast. Jiacai Liu’s excellent analysis called this a mismatch between Bun and Zig rather than a failure of Zig, and fine, I will grant the framing. But notice what that concedes: Zig is only safe when your organization behaves like TigerBeetle. Rust is safe when your organization behaves like a normal company full of normal humans under normal deadline pressure. One of these is a realistic assumption about software teams. The other is a monastery admission requirement.
The compiler was always the AI
Here is the reframe that made everything click for me. The Rust compiler was the AI of the pre-AI era. It does not generate code, it proves things about code. The trait solver performs logic resolution that is directly descended from Prolog, the original AI language. Rust’s next-gen trait solver, Chalk, was literally designed as a Prolog-style logic engine. Every time rustc rejects your borrow, a small theorem prover is telling you your reasoning is broken before reality does.
And this is exactly why Rust plus LLM is such a violent combination. An AI generating C or Zig gets feedback from tests, sanitizers, and eventually crashes in production. An AI generating Rust gets a formal verdict on memory and thread safety in seconds, for free, on every iteration. The compiler becomes a tireless, deterministic reviewer inside the generation loop.
Bun’s rewrite is the existence proof. The migration surfaced roughly 16,000 compiler errors when the codebase was split into crates, and the team pointed dozens of parallel Claude agents at them, using cargo check output as the feedback signal. The result at merge: about 4% of the Rust code in unsafe blocks, and 78% of those are single lines wrapping C++ FFI boundaries. Nineteen known regressions, all fixed. Test compatibility hit 99.8% on Linux x64 during the port. Jarred started it as a throwaway experiment, expecting the code to be deleted. A few days in, the experiment ate the roadmap.
The whole thing landed in roughly the time it takes most teams to schedule the kickoff meeting for a rewrite that never ships.
Two visions, one future
So now hold the two worldviews side by side.
Kelley, in his JetBrains interview, called AI contributions “invariably garbage,” banned them in Zig’s code of conduct, called paying for cloud AI coding an insane proposition, and said his bar is “uncompromising perfection.” Zig is at version 0.16 after eleven years. No 1.0 in sight, by design, polished slowly by a handful of people.
Sumner took a 535K line codebase drowning in memory bugs, pointed a frontier model plus rustc at it, and shipped a working Rust port with a fraction of the unsafe surface and the entire use-after-free bug class deleted from his future.
One of these people is optimizing for the craft of writing code. The other is optimizing for the outcome of software that works. And I say this with real sadness, because the craft version is the one I fell in love with: the outcome guy is right.
Is the new Bun perfect? No. Nobody on that team has read all 6,755 commits, and the skeptics are correct that the first weird concurrency bug six months from now will be a genuine test. But that risk exists on top of a foundation where the compiler guarantees an entire vulnerability class cannot exist. The old risk was the same unread-corners problem plus segfaults. Strictly worse.
Where that leaves me
I get why Kelley is bitter. Anthropic bought his flagship user in December 2025, the donations stopped, and within months his language’s poster child publicly walked out the door. The blog post reads like a breakup letter, and honestly, some of the interpersonal grievances in it are probably legitimate.
But “your code was garbage” is not a defense of Zig. It is an indictment of any language whose safety story is “be better.” The industry ran that experiment for fifty years with C. Google ran it for fifteen years with Binder. Bun ran it for five years with Zig. The result is always the same, and Rust exists precisely because someone finally decided to stop rerunning it.
I know what this means for people like me who came up through the artisanal era. The 11-day rewrite is the writing on the wall for a lot of what we called craftsmanship. I am not thrilled about it. But I watched the wheel turn, and I would rather be on it than under it.
Zig was my first love. Rust is the one I ship. There is no way back.
This piece is part of what I’m building at **leestack.dev** — a space where I get obsessive about system design, architecture, and engineering strategy. Interactive labs are in the works. Probably worth a tab you won’t close.

Originally published at https://leestack.dev.
Reference: Rewriting Bun in Rust;
메타데이터
- post_id
- dc7e752eee7f
- slug
- no-way-back-to-zig-dc7e752eee7f
- url
- https://medium.com/@lordmoma/no-way-back-to-zig-dc7e752eee7f
- canonical_url
- https://medium.com/@lordmoma/no-way-back-to-zig-dc7e752eee7f
- author_url
- https://medium.com/@lordmoma
- status
- ok
- fetched_at
- 2026-07-21 04:28:33