Bun's Zig→Rust Rewrite in 11 Days: The Loop, Not the 64 Claudes
In eleven days in May 2026, Bun's 535,496 lines of Zig became Rust. One engineer, roughly 64 Claude instances, 6,502 commits, about $165,000 in API tokens. The headline wrote itself, and most of the coverage since has argued about whether the headline is true. That argument is the least interesting part. The transferable thing is the loop Jarred Sumner built to make it work — and the fact that, twelve weeks on, the rewrite still has not shipped to users.
TL;DR
- The loop is the lesson, not the 64 Claudes. One implementer, two reviewers that see only the diff and are told to assume it is wrong, one separate fixer. The parallelism is a cost decision; the split context window is the engineering.
- The spec came first and it was written by the model. Three hours of conversation became
PORTING.md; a whole workflow analysing every struct field in the codebase becameLIFETIMES.tsv. Sixty-four agents produced consistent code because they were all bound to the same artefact. - The compiler and the test suite were the ground truth. ~16,000 compiler errors became a work queue. 1.4 million
expect()calls became the acceptance gate, with zero tests deleted or skipped. - The regressions are the best free lesson in the whole story. All four classes come from the same root: the port assumed two languages agreed about something they did not.
- The case against is real and partly verifiable. Zig's creator argues the diagnosis was wrong; a Miri report found UB in "safe" Rust days after the merge; and as of today
bun@lateston npm is still 1.3.14, the last Zig release. - Nothing here scales down cleanly except the loop. You cannot spend $165,000 on a refactor. You can absolutely stop letting the agent that wrote the code review the code.
What actually happened
Bun is the JavaScript runtime, bundler and package manager that Anthropic acquired in December 2025, largely because Claude Code runs on it. In May 2026 Sumner ported the entire codebase from Zig to Rust: 1,448 .zig files became .rs, a diff of +1,009,272 lines landed across 6,502 commits between 3 and 14 May, peaking at 695 commits in a single hour. The work used Claude Fable 5 — pre-release at the time — driven by Claude Code's dynamic workflows.
The stated reason was memory safety, and the bug list is specific rather than abstract: use-after-free in node:zlib, node:http2 and UDP sockets; double-free in CSS parsing; leaks in crypto.scrypt, fs.watch() and TLS; a race on concurrent MessageEvent access. The common factor is the seam where garbage-collected JavaScript values meet manually managed native memory.
Sumner's framing of why a style guide was not enough:
We could have kept fixing these kinds of bugs one-off in perpetuity, but we owe it to our users counting on us to do better than that.
Zig hands cleanup to the call site through defer. Rust attaches it to the type through Drop. In a codebase whose entire job is holding native resources on behalf of a garbage collector, that is the difference between a rule you have to remember 1,448 times and a rule the compiler remembers for you. Whether that justified a rewrite is exactly what the critics dispute, and I will come back to it.
The loop
This is the part worth stealing, and it is almost independent of the scale it ran at.
The spec was an artefact, not a prompt
Before any code moved, Sumner spent about three hours talking to Claude about how Zig patterns should map to Rust, then had it serialise the conversation into PORTING.md. A second workflow was pointed at the lifetime problem directly:
analyze the proper lifetimes of every struct field in the codebase. This workflow should read every struct field within every single file and trace the control flow.
That workflow proposed a lifetime per field, ran the proposals past two adversarial reviewers, applied the feedback, and serialised the result to LIFETIMES.tsv "for other claudes to look at." A final review pass checked the two documents against each other for conflicts. Sumner read both by hand.
From then on the instruction to every porting agent was, in effect, match PORTING.md and LIFETIMES.tsv.
This is the load-bearing move. Sixty-four agents working from sixty-four independently reasoned interpretations of "port this to Rust" produce sixty-four dialects. Sixty-four agents working from one written spec produce something a human team can still read. The spec is also reviewable in a way that a million-line diff is not — which matters, because Sumner poses the obvious question himself: "How do you review a PR with +1 million lines added?" You do not. You review the thing that generated it.
The reviewer must not be the author
The loop that ran on every unit of work — a file, a crate, a failing test — was four agents in three roles:
- One implementer. Full context: the original Zig, the spec, its own reasoning.
- Two adversarial reviewers. Each gets the diff and nothing else — no implementer reasoning — and is told to assume the code is wrong.
- One fixer. Applies everything the reviewers found. Only then does the code land.
Sumner's justification is the sharpest sentence in the post, and it is not really about AI:
The person writing the code wants to merge the code, which can bias their actions to ship before it's ready. Claude is the same way.
Three bugs the blind reviewers caught before merge, all of which would have passed a self-review:
- An async-close use-after-free. A
Box<uv::Pipe>dropped at the end of a match arm while libuv still held the pointer for its close callback — free, then use, then free again. The fix wasBox::leak(pipe).close(Subprocess::on_pipe_close), handing ownership to the callback. trunc()wherefloor()was needed. File mtimes before 1970 are negative, and truncating toward zero produced atimespecwith a negative nanoseconds field — structurally invalid, and invisible until someone's file was old.- Eager evaluation in
unwrap_or.unwrap_or(1.0 - second.percentage.unwrap())evaluates its argument whether or not it is needed, so the innerunwrap()panicked on the path that was supposed to be safe.unwrap_or_elsewith a closure fixed it.
None of these are exotic. All three are the kind of thing an author skims past because they already know what the code was supposed to do. That is precisely the knowledge the reviewer is denied.
The compiler was a work queue
After translation, cargo check reported roughly 16,000 errors. That is not a disaster in this method — it is the backlog, and it is already partitioned. Group by file and crate, shard the crates across four git worktrees, run 16 loops per worktree, and let each loop own one crate end to end through the same implement/review/fix cycle.
The interesting failure here was structural, not mechanical: the initial split into crates had cyclical dependencies, so a separate set of workflows had to classify where the cyclic code belonged before the split could be redone. Machine-scale parallelism does not remove architecture decisions; it just makes you hit them sooner.
The test suite was the acceptance gate
Bun's suite is unusually large — on Debian 13 x64, 1,386,826 expect() calls across 60,624 tests in 4,174 files — and the rule was absolute: 0 tests skipped or deleted.
Locally, the loop sharded ~100 random test files per worktree, ran them, saved the failing stacktraces to a file, and fed those into the same implement/review/fix cycle. Then it moved to CI across six platform/architecture combinations sharded over Buildkite. Two days after the first CI run, failing test files went from 972 to 23. Linux went green on 10 May; everything went green on 14 May, build #54202.
The suite is doing something specific here that is easy to miss. It is not proving the Rust is good. It is proving the Rust does what the Zig did, which is the only property a port actually owes you.
The unglamorous half
The write-up is honest about the parts that were just operations, and they took real effort:
- Agents fighting over git. The first attempt failed because "one Claude ran
git stashbefore committing. Another rangit stash pop. And thengit reset HEAD --hard. They were stepping on each other!" The fix was procedural: commit named files only;git stash,git resetand slow commands like a mid-workflowcargo checkwere forbidden outright. - Agents stubbing instead of fixing. A recurring failure mode, handled by a rule with teeth: "If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code."
- Tests that eat the machine. The suite includes 10,000-process spawn stress tests, minute-long leak tests and gigabyte-scale I/O. Runs were isolated with
systemd-runcgroups for memory, CPU and PID namespacing. The machine still crashed several times from disk exhaustion.
And one principle sits above all of it:
fixing the process that generates the code instead of hand-fixing the code
When output is wrong at this volume, patching the output is a treadmill. You patch the spec, the role prompts, or the rules — then regenerate.
What it bought
| Bun v1.3.14 (Zig) | Bun v1.4.0 (Rust) | |
|---|---|---|
Bun.serve throughput, Linux x64 |
169.6k req/s | 177.7k req/s (+4.8%) |
| fastify | 91.5k req/s | 95.9k req/s (+4.8%) |
next build |
13.62s | 13.03s (+4.5%) |
| tsc | 0.94s | 0.89s (+4.7%) |
| Binary size, Linux | 88 MB | 70 MB |
| Binary size, Windows | 94 MB | 76 MB |
Bun.build() × 2,000 in-process |
6,745 MB | 609 MB |
| Claude Code startup, Linux | 517 ms | 464 ms |
The memory row is the one that matters. Roughly 3 MB leaked per in-process build; a dev server that bundles on every file change is a machine that slowly dies. Drop is what makes that class of leak fixable systematically rather than one report at a time.
Two of the performance claims come with caveats worth stating. Cross-language LTO — inlining between Rust and the embedded C/C++ (JavaScriptCore, uWebSockets, BoringSSL, SQLite, lshpack/lsquic) — works because rustc emits LLVM IR. That is a genuine benefit of the toolchain, but Zig also emits LLVM IR, and Zig's own team says the option was there all along. The stack-space improvement is more clearly structural: LLVM's lifetime.start/lifetime.end intrinsics let the compiler reuse stack slots automatically, replacing manual splitting of large parser functions.
The safety story is not absolute either. The Rust codebase carries ~13,000 unsafe keywords across ~27,000 lines — about 4% of 780,000 lines — though roughly 78% of unsafe blocks are a single line, typically a call into a C library. Memory-safe-ish, at the seams where it always was.
The four regressions, and why they are the best part
Nineteen known regressions came out of the rewrite, all since fixed. Four classes are documented, and they rhyme in a way that is genuinely useful to anyone porting anything:
debug_assert! is not assert(). Zig's assert() runs in every build; Rust's debug_assert! compiles away in release. A call to insert_stale() lived inside one, so hot module reload silently stopped working in release builds only. Side effects inside an assertion are a bug in both languages — but only one of them lets you get away with it.
Silent truncation became a panic. Zig's reinterpretSlice ignored a trailing odd byte when reinterpreting to UTF-16. Rust's bytemuck::cast_slice panics instead. Blob.text() on odd-length UTF-16 went from quietly wrong to loudly dead, and now needs an explicit length check.
Bounds checks came back. Zig shipped with ReleaseFast, which omits them. Rust keeps them in release. A module-resolver overflow block sized at 64 bytes should have been 2,048 — an off-by-one that had been faithfully ported and had simply never been reachable before.
comptime had no equivalent. Zig's compile-time format strings let Bun rewrite ANSI markers before argument substitution. Rust needs a macro, which runs in the other order, and bun update -i broke.
The pattern: every one of these is a place where the port was correct as a translation and wrong as a program, because the two languages disagreed about something the translation treated as equivalent. Assertions, overflow behaviour, bounds checking, evaluation order. That list is worth keeping whether or not an LLM is doing your porting — it is the list of things a human doing a mechanical port gets wrong too.
The deliberate decision underneath all of it: the Rust is a faithful mechanical translation, not idiomatic Rust. "For myself and the team, our new Rust codebase feels very similar to the old Zig codebase." That is a real trade — it keeps the existing team productive on day one and defers the idiomatic rewrite indefinitely.
The case against
I would not trust this post if it stopped there, and the criticism is substantive enough that it changes what you should conclude.
Zig's creator says the diagnosis was wrong. Andrew Kelley's response hit 817 points on Hacker News — more than the announcement it replied to. Stripped of the personal register, his technical argument is a false-dilemma charge: the choice was never "style guide versus borrow checker," it was whether to dedicate engineering resources to the problem at all. He notes internal contradiction — if the test suite is comprehensive enough to validate a million-line port, why was the Zig full of bugs? He points out that the binary-size work is mostly comptime cleanup that had nothing to do with the rewrite and that Zig's team had recommended for years, that LTO was always available, and that the post never mentions build times, which is the trade Rust demonstrably loses. He also says that in direct conversation the Bun team had told them they were not fuzzing, which sits awkwardly beside the 24/7 fuzzing described in the write-up.
You do not have to accept his account of Bun's engineering culture to notice that the omissions he names are real omissions. The post reports no compile-time metrics at all.
Miri found UB in safe Rust three days after the merge. Issue #30719 — "PathString::slice dangling reference UB - add Miri to CI" — reduced to a nine-line repro: from_raw_parts over a pointer whose allocation had already been dropped, reachable from entirely safe code. It drew 488 points on HN under the title "codebase fails basic miri checks, allows for UB in safe rust." It was closed on 17 May, and Miri coverage is now listed as ongoing work. Worth holding both halves: the memory-safety claim was overstated at announcement, and the report was fixed in three days.
The finish line moved. Tom Lockwood's 27 July analysis counted open robobun PRs climbing from 1,277 on 9 July to 2,475 on 27 July, with no release tag since the merge, and argued the true cost is far above $165,000 once infrastructure and salaried time are counted.
I checked that claim myself today, 7 August 2026, because it is checkable:
$ curl -s https://registry.npmjs.org/bun | jq .'dist-tags'
{ "latest": "1.3.14", "canary": "1.3.13-canary.20260425.1" }
bun@latest is 1.3.14, published 13 May 2026 — the last Zig release. There is no bun-v1.4.0 tag in the GitHub repository. Open PRs authored by robobun now stand at 2,985, up from 2,475 eleven days ago; total open PRs, 3,863.
So the accurate statement, twelve weeks after the merge, is narrower than "Bun was rewritten in Rust in 11 days": the translation took 11 days and passed the suite on every platform, Rust Bun has been in production since Claude Code v2.1.181 on 17 June and under Prisma Compute, and the public release has not shipped. The convergence-to-shippable tail is longer than the port, and it is still running.
None of this makes the loop less useful. It makes the timeline claim mean something more specific than it sounds like.
What I take from this
Five things, in descending order of how confident I am and how well they scale down.
1. Never let the agent that wrote the code review the code. This is the whole post in one line, it costs nothing, and it applies to a 200-line PR as readily as a million-line one. Give the reviewer the diff and no reasoning, and instruct it to find the way the code is wrong rather than to check whether it is right. The three bugs above are all self-review blind spots, not capability gaps.
2. Write the spec down before generating anything, and review the spec instead of the output. PORTING.md and LIFETIMES.tsv are the reason 64 agents produced one codebase. At any scale, the artefact you can actually read is worth more than the output you cannot. If you find yourself reviewing generated code line by line, you are reviewing the wrong thing.
3. Point the work at something that can say no. A type checker, a compiler, a test suite, a fuzzer. Bun had ~16,000 compiler errors and 1.4M assertions to converge against, and that — far more than model quality — is why this worked. The corollary is uncomfortable: on a codebase without that gate, the same method produces a million lines nobody can validate. The test suite is the precondition, not a detail.
4. Fix the generator, not the output. Hand-patching agent output at volume is a treadmill you cannot get off. Change the spec, the role prompts, or the rules, then regenerate.
5. Discount the timeline, keep the method. "11 days" is the translation. The tail — CI convergence, Miri, the release that still has not shipped — is where the rest of the work went. Anyone budgeting a migration off this story should budget for the tail, which is the part that is not obviously compressible by parallelism at all.
There is a sixth I hold more loosely. The economics are genuinely new: Sumner estimates "3 engineers with full context on the codebase about a year" against 11 days and $165,000, during which feature work would have frozen. Mitchell Hashimoto's version — that no engineer on that salary would have hit those milestones in 11 days — is fair as far as it goes. But the counter-argument from the HN thread is fair too: those three engineers would have produced idiomatic Rust, and Bun explicitly did not. The bill for that arrives later, as maintenance on 780,000 lines of Rust that is shaped like Zig.
What is not in dispute is that a migration nobody would have authorised became a migration somebody did. "Without AI assistance, we never would've done that." That sentence, rather than any benchmark in the post, is the thing that changes how I plan work.
When this doesn't apply to you
Plainly, because the honest answer is not "everyone."
If your codebase does not have a test suite that would catch a wrong port, none of this transfers. The loop is a convergence mechanism, and convergence needs a target. Bun's 1.4 million assertions are the load-bearing element, and most codebases do not have the equivalent.
If your migration is not mechanical, the shape breaks too. A Zig-to-Rust port has a defensible one-to-one mapping — which is exactly why PORTING.md could be written in three hours and why the result could be validated by an existing suite. A migration that changes the architecture has no such mapping and no oracle.
And if the thing you are considering is a refactor rather than a port, the parallelism argument mostly evaporates. Sixty-four agents were viable because 1,448 files could be worked on independently. Sequential work with cross-cutting dependencies gets you the token bill without the wall-clock win.
Below that threshold, one thing still survives intact: split the context window. The implementer and the reviewer should never be the same agent, and the reviewer should never see why the implementer thought it was right.
FAQ
Did Bun really get rewritten from Zig to Rust in 11 days?
The translation did. Between 3 and 14 May 2026, 535,496 lines of Zig across 1,448 files became Rust in 6,502 commits, and CI went green on all six platform/architecture targets on 14 May. What did not happen in 11 days is shipping: as of 7 August 2026, bun@latest on npm is still 1.3.14, the final Zig release, and there is no v1.4.0 tag on GitHub. Rust Bun has been running in production inside Claude Code since 17 June.
How much did it cost?
About $165,000 at API pricing — 5.9 billion uncached input tokens, 72 billion cached reads, 690 million output tokens. That figure covers tokens only. Third-party analysis argues the fully loaded cost, including CI infrastructure and salaried engineering time before and after, is several times higher.
What is the adversarial review loop?
Four agents in three roles per unit of work: one implementer writes the code with full context; two reviewers receive only the diff, with none of the implementer's reasoning, and are instructed to assume the code is wrong; one separate fixer applies their findings before anything lands. The point is that an agent which wrote code is biased toward merging it, so the reviewer must be a different agent with a different context window.
What were PORTING.md and LIFETIMES.tsv?
The binding spec. PORTING.md came out of about three hours of conversation about how Zig patterns should map to Rust. LIFETIMES.tsv came from a workflow that read every struct field in the codebase, traced control flow, proposed a Rust lifetime for each, and ran those proposals past adversarial reviewers. Every porting agent was instructed to match both.
Is the Rust version idiomatic Rust?
No, deliberately. It is a faithful mechanical translation that stays structurally close to the Zig, so the existing team could keep working in it immediately. Refactoring toward idiomatic Rust is deferred to after v1.4.
Is Rust Bun actually memory-safe now?
More so, not absolutely. It carries about 13,000 unsafe keywords across ~27,000 lines (roughly 4% of the codebase), mostly single-line calls into embedded C/C++. Three days after the merge, a Miri report found undefined behaviour reachable from safe Rust; it was fixed within three days, and Miri coverage in CI is listed as ongoing work.
What did Zig's creator say?
Andrew Kelley argued that Bun's bugs came from engineering practice rather than from Zig, that "style guide versus borrow checker" is a false dilemma when the real answer was dedicating engineering resources, and that the announcement omitted trade-offs — notably build times, which Rust loses, and the fact that the binary-size and LTO gains were available in Zig all along.
Should I try this on my codebase?
Only if you have an oracle. The method converges against compiler errors and a test suite; without a suite that would catch a wrong port, you get volume with no way to validate it. The one part that transfers at any size, for free, is separating the implementer from the reviewer.
References
- Rewriting Bun in Rust — Jarred Sumner, Bun blog, 8 July 2026. Every unattributed figure and quote above comes from here. HN discussion, 795 points.
- My thoughts on the Bun Rust rewrite — Andrew Kelley, July 2026. HN discussion, 817 points.
- PathString::slice dangling reference UB — add Miri to CI — oven-sh/bun issue #30719, opened 14 May 2026, closed 17 May 2026. HN discussion, 488 points.
- How is the Bun rewrite in Rust going? — Tom Lockwood, 27 July 2026. HN discussion, 496 points.
- The Pulse: What can we learn from Bun's rapid Rust rewrite with AI? — Gergely Orosz. Source of the Hashimoto comparison.
- Bun is joining Anthropic — 2 December 2025.
- Release state verified directly on 7 August 2026 via the npm registry (
bun@latest= 1.3.14) and the GitHub tags and PR search APIs.
Written 7 August 2026, twelve weeks after the merge and one month after the announcement.
Related reading on this site: herdr, a Rust agent runtime, x402 native payments, simpleconf, more AI posts, and all posts.