Bun's 11-Day, $165K AI Rewrite from Zig to Rust Just Passed Every Test
On May 14, 2026, the Bun team did something that almost every senior engineer would tell you to "absolutely never do" — rewrite 535,496 lines of production-grade Zig code into Rust. Even crazier, the entire process took only 11 days, led by a single person, executed in parallel by 64 Claude agents, with a total API cost of approximately $165,000.
The result? Zero tests deleted, all 60,000+ tests passing, binary size reduced by 20%, memory leaks essentially eliminated, throughput improved by 2-5%, and 128 historical bugs fixed.
This is not a story about "Rust being better than Zig." This is a story about "what becomes possible when AI is powerful enough."
The Starting Point: A Crazy Project That Took Off on Zig
Rewind to 2021. Jarred Sumner, working alone in a cramped apartment in Oakland without LLM assistance, wrote the first version of Bun in Zig.
He put it bluntly: "For an overly ambitious, single-person project, the default outcome is joining the graveyard of dead side projects on a GitHub profile. Zig made Bun possible."
He bet right. Today, Bun's CLI downloads exceed 22 million per month. Tools like Claude Code and OpenCode depend on it as a runtime. Vercel, Railway, and DigitalOcean all offer native support.
But the other side of growth is the cost of stability.
II. The Stability Black Hole: When Manual Memory Management Meets GC
Bun's positioning dictates its complexity:
- Transpilation, minification, and bundling of JavaScript/TypeScript/CSS
- An npm-compatible package manager
- A Jest-compatible test runner
- Node.js API implementations (fs, net, tls, etc.)
- HTTP/1.1 and WebSocket clients
- A module resolver
The core of the problem: JavaScript is a garbage-collected language, and Zig (like C) does not manage your memory.
Jarred posted a shocking bug list in the article — fixes from just one version, v1.3.14:
- A heap-use-after-free in
node:zlibcaused by a race between calling.reset()and an asynchronous.write()- Re-entering a JS callback in
node:http2triggered a hash table rehash, invalidating internal stream pointers- The
valueOf()/toString()callback inUDPSocket.send()detached the ArrayBuffer between payload capture and the actual sendtlsSocket.setSession()leaked one SSL_SESSION (about 6.5 KB) on every call- The watcher in
fs.watch()was never garbage-collected after.close()- A double-free in the CSS parser for
background-clipwith multiple vendor prefixes and multiple background layers
This is not an isolated case. It is a systemic consequence of a hybrid GC + manual memory management architecture.
Zig's "Virtue" Is Also Its "Flaw"
One of Zig's core design principles is "no hidden control flow." It uses defer and errdefer to explicitly clean up resources, rather than running destructors automatically at the end of a scope like C++ destructors or Rust's Drop.
| Language | Cleanup Mechanism |
|---|---|
| Zig | defer, errdefer |
| C++ | Destructors, move semantics |
| Rust | Drop trait |
The problem: When you pass a *T to multiple functions, how do you know when it is no longer referenced? When is it safe to free?
Bun's countermeasure was a mix of arena lifetimes, reference counting, and one fatal principle — "review very carefully."
For 530,000 lines of code, "very carefully" is not a promise you can keep.
II. "Stay Sane, Don't Make Mistakes" Is Itself a Bug Pattern
The Bun team was already doing industry-leading stability work:
- Patching the Zig compiler to add Address Sanitizer support, running ASAN on every commit
- Using Zig's ReleaseSafe compilation on Windows (retaining safety checks)
- Running 24/7 continuous fuzzing on the JavaScript engine with Fuzzilli
- Extensive end-to-end memory leak tests
This was already more than most projects do. But bugs still kept appearing.
In the words of a user — "I don't want to go to bed every night worrying about Bun crashing."
Introducing smart pointers, establishing coding style guidelines — these were all options. TigerBeetle has TigerStyle, Google has a 31,000-word C++ style guide. But style guides are enforced by code review, code review relies on people, and people are unreliable.
Jarred also voiced his own concern:
Writing smart pointers in Zig loses the compiler guarantees of Rust while taking on all the complexity.
III. Why Not C++?
Bun already embeds a large number of C/C++ libraries: JavaScriptCore, uWebSockets, BoringSSL, SQLite. About 20% of the code was already C++.
C++ would bring constructors/destructors and could delete a large amount of extern "C" glue code. But Jarred's judgment was: C++ still relies on style guides and manual review, and memory issues would still slip through.
Compiler errors are the best feedback loop. This is Rust's core argument.
IV. That Crazy 11 Days
The key question here is not "Is a Rust rewrite good?" but "Can AI help us do this?"
Jarred's original plan was to fix stability in Zig using Rust-style smart pointers — but honestly, he didn't want to. So an idea emerged: "Could I spend a week testing whether Anthropic's new model could rewrite Bun into Rust?"
He initially didn't think it would succeed. A few days later, a high percentage of the test suite started passing.
Preparation: A 3-Hour Conversation
Jarred spent about 3 hours discussing with Claude how to map Zig patterns to Rust. This conversation was serialized into a PORTING.md file, which later made it onto Hacker News.
The next question: How to map manually managed memory code to Rust lifetimes?
His approach was to write a "dynamic workflow" where Claude would:
- Read every field of every struct in the codebase, tracing control flow
- Propose lifetime suggestions for each field with a complex lifetime
- Use 2 adversarial review agents to audit the suggestions
- Apply feedback and serialize into a
LIFETIMES.tsvfile for other Claudes to reference
Then another round of adversarial review, cross-checking PORTING.md and LIFETIMES.tsv.
He also manually read through it all himself.
Trial Run + Various Train Wrecks
Before asking Claude to translate all 1,448 .zig files, they tried 3 files first. The workflow for each file: 1 implementer writes .rs, 2 adversarial reviewers check, 1 fixer applies changes.
Then came the train wrecks:
- Two Claudes stepped on each other — one ran
git stash, the other rangit stash pop - Another ran
git reset HEAD --hard - Putting them in isolated worktrees ran out of disk space
Jarred's solution: Modify the workflow instructions to forbid any git command that wasn't a "single-file atomic commit." Then split into 4 worktrees, each running 16 Claudes.
Peak Code Writing: 1,300 Lines Per Minute
At peak parallelization, Claude wrote about 1,300 lines of code per minute. Every single line was reviewed by 2 adversarial reviewers + modified by 1 fixer before being committed.
The result: Not a single line could run. This was expected behavior.
He also discovered a hardware problem — the default IOPS on the EC2 instance were insufficient. A single grep command could freeze disk reads/writes for several minutes.
Compiler Errors as a Work Queue
After writing all the code, the next workflow was crate-level cargo check error fixing.
The biggest pitfall was circular dependencies. The Zig codebase was a single compilation unit (equivalent to one crate), while Rust needed to be split into about 100 crates. Jarred's previous PR attempting to pre-process this was insufficient, so another workflow was run to classify the ownership of circular dependencies.
This round revealed about 16,000 compile errors. An astronomical number for a human, not for 64 Claudes.
His workflow pattern:
- Run
cargo checkfor each crate, save errors grouped by file - Fix all compiler errors within that crate
- 2 adversarial reviewers audit
- 1 fixer applies fixes
Another train wreck: Claude interpreted "make all crates compile" as "stub out functions with compile errors," and started adding long comments to rationalize the workarounds.
Jarred's countermeasure: Added a rule to the adversarial reviewer's instructions —
"If you need to write a paragraph explaining why a workaround is acceptable, the code is wrong — fix it."
From Smoke Test to All Passing
After compiling, linker errors, panics, and individual CLI subcommands were gradually fixed.
Then tests were run. First locally, then in CI.
This stage used stronger isolation: systemd-run (cgroups) to limit memory and CPU, isolate PID namespaces. The machine still ran out of disk a few times.
Two days later, Linux CI was all green. From 972 failing test files down to 0.
Final Data
| Metric | Value |
|---|---|
| Time | 11 days (May 3 → May 14) |
| Commits | 6,778 |
| Peak Parallelism | 64 Claudes |
| Input Tokens | 5.9 billion (uncached) |
| Output Tokens | 690 million |
| Cache Reads | 72 billion |
| API Cost | ~$165,000 |
| Tests Deleted | 0 |
| Human Equivalent Effort | 3 full-context engineers × 1 year |
"Doing this manually, 3 engineers fully familiar with the codebase would need about 1 year. During that time we couldn't fix bugs, couldn't add features, couldn't do security fixes. This was simply not a viable option. The realistic choice was: do nothing, and forever fix the bugs mentioned at the beginning of the article."
V. Real Gains After Migrating to Rust
5.1 Significant Memory Usage Reduction
The core reason — Drop.
Zig's defer requires writing explicit cleanup code at every call site. One omission is a leak; writing it twice on an error path is a double-free. Rust's Drop runs automatically when a value goes out of scope.
The most obvious effect: Bun.build() no longer leaks memory under repeated calls.
// Build the same project 2000 times in the same process
for (let i = 0; i < 2000; i++) {
await Bun.build({
entrypoints: ["./index.js"],
minify: true,
sourcemap: "external",
});
}
| Build Count | v1.3.14 (Zig) | v1.4.0 (Rust) |
|---|---|---|
| 500 | 1,914 MB | 526 MB |
| 1,000 | 3,506 MB | 586 MB |
| 1,500 | 5,097 MB | 608 MB |
| 2,000 | 6,745 MB | 609 MB |
The problem of leaking ~3MB per build was systematically solved. A previous attempt to do the same thing in Zig (PR #24741) wasn't merged because, without a mechanism equivalent to Drop, no one could have enough confidence.
5.2 Binary Size Reduced by 20%
- Windows: 94 MB → 76 MB (-19%)
- Linux: 88 MB → 70 MB (-20%)
Partial reason: Zig's comptime was overused; Rust's representation is more economical.
Combined with ICU data optimization, Identical Code Folding, and other linker optimizations, 12-18 MB were saved this way.
5.3 Performance Improvement of 2-5%
Rust supports cross-language Link-Time Optimization (LTO) between C/C++ and Rust, enabling cross-language inlining after dead code elimination.
| Framework | v1.3.14 | v1.4.0 | Improvement |
|---|---|---|---|
| Bun.serve | 169.6k req/s | 177.7k req/s | +4.8% |
| node:http | 103.8k | 108.5k | +4.5% |
| Elysia | 158.9k | 163.3k | +2.8% |
| Express | 64.5k | 66.6k | +3.2% |
| Fastify | 91.5k | 95.9k | +4.8% |
| next build | 13.62s | 13.03s | +4.5% |
| tsc -b --force | 0.94s | 0.89s | +4.7% |
5.4 128 Historical Bugs Fixed
v1.4.0 fixed 128 bugs compared to v1.3.14, covering memory leaks, crashes, and even help text color errors.
VI. The Cost of the Rewrite: 19 Regressions
This migration inevitably introduced 19 known regressions, each already fixed. Jarred openly acknowledged these mistakes and shared the most instructive lessons:
Side Effects in debug_assert!
Zig's assert is a function — arguments execute in all builds. Rust's debug_assert! is a macro — the entire expression is erased in release builds.
An insert_stale call was wrapped in debug_assert!, so it no longer ran in release builds. As a result, React Hot Module Replacement silently broke in certain cases. Debug builds worked fine, release builds crashed.
Differences with Odd-Length Byte Slices
Zig's reinterpretSlice(u16, bytes) uses @divTrunc, silently ignoring the last odd byte. bytemuck::cast_slice panics outright.
Blob.text() panicked the entire process when a UTF-16 BOM was followed by an odd number of bytes.
The pragmatic fix: go back to ignoring the odd byte — &buf[..buf.len() & !1].
Differences in Bounds Checks
On macOS/Linux, Zig compiles in ReleaseFast mode (removing bounds checks). Rust's release builds retain bounds checks.
The module resolver inlined long filenames into a global list, overflowing into overflow blocks. Zig's original code set each block size to 270,272, a placeholder value 30x lower than intended. Real projects hit the ceiling. Rust used a panic instead of writing out of bounds — a safe signal here.
The Difference Between comptime and Macros
Zig's comptime parameters can substitute format arguments before string parsing. Rust lacks comptime, causing Output::pretty to swallow parameters during ANSI escape code parsing.
The result: the OSC 8 hyperlink tail \ in bun update -i sat right next to the <r> tag, the tag parser ate the escape character, and printed "oxfmtr" instead of "oxfmt".
The fix: the bun_core::pretty! macro.
The high value of these regression cases: They are not "written wrong," but semantic differences between two languages — syntactically seemingly identical, semantically completely different. This is the highest-frequency error category in AI-driven cross-language migrations.
VII. Prisma's Endorsement and Claude Code's Silent Switch
Prisma launched the Prisma Compute public beta on the Rust rewrite. Their tech lead Alexey Orlenko's original words:
"We encountered memory leaks and connection pool failures after VM suspend/resume. After the Rust rewrite handled it, it perfectly managed these failure modes."
Claude Code v2.1.181 was the first release to use the Rust version of Bun. From production telemetry data: Linux p50 startup time dropped from 517ms to 464ms — a 10% speedup. "Almost no one noticed. Boring is good."
VIII. The Bigger Picture: The Software Rewrite Paradigm in the AI Era
We need to step back and look at the real significance of this event.
Not "Rust Won, Zig Lost"
Jarred repeatedly emphasized: Zig made Bun possible. "Without Zig, we wouldn't be where we are today."
This is not a language war. This is a structural deficiency of software relying on ownership and lifetime-based systematic resource management within a hybrid GC/manual memory model.
C++ can theoretically do what Rust can do. Google and Microsoft can barely manage it with style guides. But the essence of the problem is not "can this language do it," but can the compiler tell me I'm doing it wrong while I'm writing the code.
Style guides rely on people for enforcement. Compiler errors do not rely on people.
AI Turned "Impossible" into "An Engineering Problem"
This is what I consider the most important paragraph of this blog post:
"If rewritten manually, 3 full-context engineers would need about 1 year. That means 1 year of not fixing bugs, not doing security updates, not adding features. The realistic choice was: either don't rewrite, or forever fix the bugs mentioned at the beginning of the article."
AI didn't eliminate the risk of a rewrite — it reduced the product of "risk x effort" by two orders of magnitude. $165,000 in API fees, an 11-day timeline, 64 parallel Claudes — these things add up to an acceptable engineering budget for most companies, not an impossible task.
The "Claude Code Dynamic Workflow" Pattern
The core architecture of Jarred's project wasn't Rust, but Claude Code's Dynamic Workflows — organizing collaboration across 64 Claude instances into a loop:
while (task = todoList.pop()) {
result = task()
feedback = await Promise.all([review(result), review(result)])
await apply(feedback, result)
}
Key insights:
- Separate implementers and reviewers — the Claude that writes code doesn't review, the one that reviews doesn't write code
- Assign 2 adversarial reviewers per task — their only job is to "find reasons the code doesn't work"
- Fix the process, not the code — when a pattern caused problems (like long comments rationalizing workarounds), he modified the workflow rules, not manually fixed each instance
This is TDD for the AI era: not Test-Driven Development, but Review-Driven Development.
VIII. Bun's Future
Bun v1.4.0 is the first Rust version. Currently, about 4% of the code is inside unsafe blocks (about 13,000 unsafe keywords), 78% of which are single lines — pointers from C++ or calls to C libraries. This proportion will decrease over time.
Bun still embeds JavaScriptCore and still needs C/C++ interfaces. It will not become a "pure Rust project." But that part of the boundary wrapped in unsafe is easier to audit than the internal Zig code.
The safeguard systems the team already has running:
- Rust's borrow checker (fully running in CI)
- Miri (running on a growing portion of the code in CI)
- LeakSanitizer integration, tracking all native memory allocations
- 24/7 coverage-guided fuzzing covering all parsers (JS, TSX, CSS, JSON5, TOML, YAML, Markdown, INI, Bun Shell scripts, semver ranges, .patch files, CSS colors, etc.)
- Automatic PR submission: bugs found by the fuzzer are automatically sent to Claude to generate fix PRs
"An engineer a year ago could do far less than they can today."
Conclusion
Bun's Rust rewrite is a signal. Not "Zig isn't good enough" — Zig remains one of the most interesting languages for systems programming. Not "Rust is suitable for everything" — Rust's learning curve won't magically disappear. Not "AI is about to replace programmers" — this project had a top-tier engineer watching every single step.
This signal is:
Decisions in software engineering that were shelved because they were "too expensive" now need to be re-evaluated.
In the past, rewriting a 530,000-line production project was unrealistic. In the past, cross-language migration meant freezing feature development for a year. In the past, fixing memory leaks in a hybrid memory model project was more of a philosophical pursuit than an engineering goal.
These "pasts" are rapidly becoming "past tense."
Original article: Rewriting Bun in Rust — Jarred Sumner @ Bun / Anthropic
Original tech blog · Open-source project architecture deep dive · idao.fun
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
I think tools written in Rust (with unsafe accounting for less than 10%) can be switched over boldly and aggressively. After all, Rust doesn't crash easily.