跪拜 Guibai
← Back to the summary

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:

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:zlib caused by a race between calling .reset() and an asynchronous .write()
  • Re-entering a JS callback in node:http2 triggered a hash table rehash, invalidating internal stream pointers
  • The valueOf()/toString() callback in UDPSocket.send() detached the ArrayBuffer between payload capture and the actual send
  • tlsSocket.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-clip with 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:

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:

  1. Read every field of every struct in the codebase, tracing control flow
  2. Propose lifetime suggestions for each field with a complex lifetime
  3. Use 2 adversarial review agents to audit the suggestions
  4. Apply feedback and serialize into a LIFETIMES.tsv file 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:

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:

  1. Run cargo check for each crate, save errors grouped by file
  2. Fix all compiler errors within that crate
  3. 2 adversarial reviewers audit
  4. 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%

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:

  1. Separate implementers and reviewers — the Claude that writes code doesn't review, the one that reviews doesn't write code
  2. Assign 2 adversarial reviewers per task — their only job is to "find reasons the code doesn't work"
  3. 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:

"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

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

Static7

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.