Bun 1.4: Rewriting the Runtime in Rust using AI

Bun 1.4: Rewriting the Runtime in Rust using AI

Bun has rewritten its core runtime in Rust for the v1.4.0 release, moving away from Zig to systematically eliminate use-after-free and double-free crashes. This transition was achieved in 11 days by a single engineer using Claude Fable 5 and Claude Code dynamic workflows, resulting in a runtime that is smaller, faster, and more stable.

Why Bun Moved from Zig to Rust

Bun's original implementation in Zig allowed for rapid initial development and low-level control, but the project's massive scope—including a transpiler, package manager, test runner, and Node.js API implementations—led to significant stability challenges.

Systemic Stability Issues

Despite using Address Sanitizer (ASAN) and 24/7 fuzzing with Fuzzilli, Bun suffered from recurring memory safety issues. A sample of bugs fixed in v1.3.14 included:

  • Use-after-free crashes in node:zlib, node:http2, and UDPSocket.send().
  • Heap out-of-bounds writes in UDPSocket.sendMany().
  • Memory leaks in crypto.scrypt, tlsSocket.setSession(), and fs.watch().
  • Double-free crashes in the CSS parser.

The Limitation of Manual Memory Management

Zig's philosophy of explicit memory management (using defer and errdefer) requires meticulous review at every call site. For a project mixing garbage-collected JavaScript values with manually managed native memory, this created a high cognitive load and frequent errors. While style guides (like TigerStyle) can mitigate these issues, they are not enforced by the compiler. Rust's ownership model and Drop trait provide compiler-level guarantees against use-after-free and double-free errors, turning runtime crashes into compile-time errors.

The AI-Driven Rewrite Process

Rewriting 535,496 lines of Zig code manually would typically take a small team a year. Instead, Bun utilized an agentic workflow to perform a mechanical port.

Workflow Architecture

The rewrite was executed using approximately 50 dynamic workflows in Claude Code over 11 days. The process followed a structured loop: Task $\rightarrow$ Implementation $\rightarrow$ Adversarial Review $\rightarrow$ Application.

  1. Preparation: The engineer spent three hours defining a PORTING.md (mapping Zig patterns to Rust) and a LIFETIMES.tsv (analyzing struct field lifetimes).
  2. Mechanical Port: 1,448 .zig files were translated to .rs files. At peak, the AI wrote approximately 1,300 lines of code per minute.
  3. Compiler Error Resolution: Approximately 16,000 compiler errors were resolved by dividing the work among 64 concurrent Claude instances across four worktrees.
  4. Verification: The team used a language-independent TypeScript test suite with over a million assertions to verify correctness.

Adversarial Review

To prevent LLM hallucinations and bias, Bun employed "adversarial review." An implementer Claude wrote the code, while two separate reviewer Claudes—given only the diff and told to assume the code was wrong—searched for bugs. This process caught critical issues, such as an asynchronous uv_close call that would have caused a use-after-free/double-free bug.

Technical Improvements in Bun v1.4.0

Memory and Stability

The transition to Rust's Drop trait eliminated several instrumentable memory leaks. For example, in-process Bun.build() calls that previously leaked 3MB per build now level off, significantly reducing memory pressure for dev servers.

Binary Size and Performance

  • Binary Size: Bun's binary size shrank by approximately 20% on Linux and Windows (e.g., Windows decreased from 94MB to 76MB). This was achieved through the rewrite, ICU optimizations, and identical code folding.
  • Performance: Bun v1.4.0 is 2% to 5% faster. HTTP throughput for Bun.serve increased by 4.8%, and next build times improved by 4.5%. This is attributed to cross-language link-time optimization (LTO) between C/C++ and Rust.
  • Stack Usage: Recursive-descent parsers (JSON, YAML, TOML) now use less stack space due to LLVM's llvm.lifetime.start and llvm.lifetime.end intrinsics, which allow for better stack slot reuse.

Porting Challenges and Regressions

Despite the success, the rewrite introduced 19 known regressions, primarily due to semantic differences between the languages:

  • Macro Erasure: Rust's debug_assert! is a macro that erases the expression in release builds, whereas Zig's assert is a function. This broke HMR in certain React projects because a side-effecting call inside the assert was erased.
  • Slices: bytemuck::cast_slice in Rust panics on odd-length slices, whereas the original Zig helper ignored the trailing byte. This caused Blob.text() to panic on certain UTF-16 inputs.
  • Bounds Checks: Rust's release builds maintain bounds checks, which revealed an off-by-one error in the module resolver that was previously hidden in Zig's ReleaseFast mode.

Community and Industry Perspective

"LLMs excel when you have verifiable rewards. I imagine we'll get a lot more rewritten in rust projects in the future. Rust is also an ideal target for such rewrites as it offers a lot of verification (via its type system)."

While the technical results are positive, some community members expressed concern over the "lone hacker" approach to the transition and the lack of LTS support for the Zig version. Others noted that the cost of the rewrite—approximately $165,000 in API tokens—might be a significant barrier for projects not backed by a company like Anthropic.

Sources