Bun Rewrites in Rust: What It Means for the JavaScript Runtime

·9 min read·Review

Bun merged a million-line PR rewriting its core from Zig to Rust. This post covers the technical motivations, what changed in the codebase, measured performance impact, and what it signals for the JavaScript runtime ecosystem.

Bun Rewrites in Rust: What It Means for the JavaScript RuntimeAI Generated Image

On May 11, 2026, Jarred Sumner merged PR #30412 into Bun's main branch: "Rewrite Bun in Rust." 1.01M lines added across 6,755 commits. The JavaScript runtime that built its identity on Zig is now primarily Rust.

# The Numbers

Metric Before After
Primary language Zig Rust (46.6%)
Zig proportion ~70% 32.2%
C++ (JSC bridge) ~15% 13.2%
Binary size Baseline 3–8 MB smaller
Test suite Passes Passes (+ fixes flaky tests)
Performance Baseline Neutral to faster

The PR itself: 1.01M additions and 4,024 deletions. The branch name — claude/phase-a-port — hints at AI-assisted porting, consistent with Bun's acquisition by Anthropic in December 2025.

# Why Move Away from Zig?

Zig was Bun's identity — the "fast systems language that isn't C++" story. So why leave?

Jarred Sumner:

We now have compiler-assisted tools for catching & preventing memory bugs, which have costed the team an enormous amount of development & debugging time over the years.

It's about debugging time. Zig doesn't have a borrow checker. No compile-time prevention of use-after-free, no data race detection. When your runtime manages millions of objects, handles concurrent I/O, and talks to a garbage collector (JavaScriptCore), these bugs show up constantly.

The follow-up PRs confirm the scale of the problem:

  • "Fix effectively every native-code memory leak in Bun" — landed days after the merge
  • "clippy: 45 deny lints + fix 2735 violations across workspace" — 2,735 issues that Rust's linter caught automatically

None of this would've surfaced easily in Zig. Rust's tooling just hands it to you.

# What Actually Changed (and What Didn't)

From the PR description, the architectural choices are conservative:

  • Same architecture, same data structures. No redesign, no new abstractions for the sake of being "Rusty." The code maps 1:1.
  • Few third-party libraries. They didn't pull in hundreds of crates. Minimal dependencies carried over.
  • No async Rust. No Tokio, no async runtime. The event loop stays custom, tuned for JS runtime semantics. They sidestepped Rust's async complexity entirely.
  • C++ remains for JSC. The JavaScriptCore bridge is still C++. Code generation (generate-classes.ts) produces the Rust ↔ C++ bindings.

Contributors now need a specific Rust nightly toolchain (pinned in rust-toolchain.toml).

# Claude Code as the Implementation Tool

Bun was acquired by Anthropic in December 2025. Every PR in the rewrite uses a claude/ branch prefix:

PR Branch Commits Lines Changed
#30412 (main rewrite) claude/phase-a-port 6,755 +1.01M / −4,024
#30875 (memory leaks) claude/asan-system-allocator 251 +5,004 / −1,822
#30901 (bundler perf) claude/bundler-perf-2x 34 +408 / −256
#31115 (main-thread perf) claude/bundler-main-thread-perf 18 +1,324 / −1,112
#31116 (code quality) claude/clippy-deny-sweep 53 +18,626 / −23,043
Total 7,111 +1.03M / −30,257

Every branch uses the claude/ prefix. The Claude bot shows up as a reviewer on all 5 PRs. GitHub Actions stamps commits with "Generated with Claude Code."

The clippy sweep (PR #31116) gives the clearest picture of the workflow: 4 parallel AI fixer shards, 2 AI reviewers per diff that both had to approve, 9 rounds until zero violations. Commit messages reference review-round.workflow.ts — it's a programmatic loop, not someone sitting at a keyboard.

Jarred directed the work, set the goals, reviewed output, and merged. The test suite is what validates correctness — it passes on all platforms.

# Merge Timeline: 11 Days, 7,111 Commits

The entire sequence — from opening the main PR to landing the last follow-up — took 11 days:

gantt
    title Bun Rust Rewrite — PR Timeline
    dateFormat YYYY-MM-DD
    axisFormat %b %d

    section Main Port
    30412 main rewrite, 6755 commits :active, pr1, 2026-05-10, 2026-05-17

    section Follow-up
    30901 bundler perf               :pr3, 2026-05-16, 2026-05-17
    30875 memory leaks, 251 commits  :pr2, 2026-05-17, 2026-05-19
    31115 main-thread perf           :pr4, 2026-05-19, 2026-05-20
    31116 clippy sweep               :pr5, 2026-05-19, 2026-05-21

That's 646 commits/day on average. The main PR alone landed 965/day. Only makes sense with AI generating in bulk while CI validates and Jarred reviews.

After the main merge, the follow-up sprint (May 17–21) knocked out quality in 4 days:

  1. Bundler hot-path profiling and fixes (34 commits)
  2. Native memory leak detection and repair (251 commits)
  3. Performance gap vs Zig closed (18 commits)
  4. 45 clippy lints enforced, 2,735 violations fixed (53 commits)

# Performance

Jarred's initial line was "benchmarks are between neutral and faster." The follow-up PRs paint a more specific picture.

# Bundler Benchmark: 10,000 Modules

Using the rolldown-benchmark apps/10000 suite on linux x64, 64-core:

Build Wall Time Total CPU Main Thread vs Zig Baseline
Rust (initial port) 3,819 ms 3,065 ms 470 ms 1.10× slower
Rust (after PR #30901) ~3,600 ms ~2,800 ms ~445 ms ~1.06× slower
Rust (after PR #31115) 3,443 ms ~2,680 ms 429 ms 1.05× faster
Zig v1.3.14 baseline 3,398 ms 2,417 ms 422 ms

Interesting bit: total CPU is lower than Zig even when wall time was higher. The bottleneck wasn't compute — it was main-thread serialisation blocking the parallel work from actually running in parallel.

# What Fixed It

PR #31115 went after the hot-path regressions one by one:

  • load_node_modules self-time: 1.28% → <0.26% (eliminated path allocation overhead)
  • Tree-shaking allocations: eliminated ~1.1 million alloc+free pairs by iterating by index instead of cloning per-part Vecs
  • Path struct size: 120 bytes → 56 bytes (stopped storing PathName inline, compute on demand)
  • MatchResult copying: 300-byte struct was copied by value through 7 function levels — now passed as &mut out-parameter
  • 48 spurious #[cold] annotations on bundle_v2.rs — the Zig-to-Rust port marked every public method #[cold], which told LLVM to deprioritise the hot path
  • Part::is_live field: moved to a per-file AutoBitSet so mark_part_live checks a bit before loading the full 272-byte Part struct
  • dir_info_cached_maybe_log stack frame: 8.6 KB → 72 bytes (outlined the cache-miss tail)

# Memory Leaks

PR #30875 (251 commits, 5,004 lines) made Bun "effectively leak-free."

The root cause was sneaky: Zig builds used libc malloc under ASAN, so LeakSanitizer could see every allocation. The Rust port unconditionally used mimalloc with MI_TRACK_ASAN=1, which only annotates shadow state — LeakSanitizer couldn't see most leaks at all.

Fix: #[global_allocator] is now gated on cfg(bun_asan). ASAN builds use std::alloc::System (libc malloc), giving LeakSanitizer full visibility. Once the leaks were visible, they turned up everywhere:

  • Bundler & linker — graph columns, chunk data, CSS rule slabs, sourcemap tables
  • Parser / AST / CSS — arena-stranded heap fields on reset
  • Package manager — Task request/data union arms, manifest cache, workspace cache
  • Dev server — transpilers, arena-stored chunks, framework projections
  • Runtime — FetchTasklet, ReadableStream, subprocess refcount imbalances
  • VM teardown — RareData stores, transpiler heap fields, timer draining

# Code Quality: 2,735 → 0

PR #31116 turned on 45 deny-level clippy lints and fixed every violation:

Fix Category Count
// SAFETY: comments on unsafe blocks 1,341
Function signature changes (by-value → by-ref) 840
Unsafe pointer deref fixes (not_unsafe_ptr_arg_deref) 156
mem_forgetManuallyDrop/Box::into_raw 47
Dead code removed (net LOC) −6,200

The lint categories show what the initial port got wrong:

  • Soundness: undocumented_unsafe_blocks, mem_forget, cast_ptr_alignment, uninit_vec
  • Performance: redundant_clone, needless_collect, large_types_passed_by_value, large_stack_frames
  • Disallowed std methods: std::sync, std::fs, std::collections all routed through Bun's own thread-safe, wyhash-backed wrappers

Miri (Tree Borrows model) CI was also added for all FFI-free crates — formal verification of memory aliasing.

# What This Means If You Use Bun

Right now: nothing breaks. Canary builds pass the full test suite. Try it with bun upgrade --canary. Stable releases don't include the Rust code yet.

Soon: fewer mystery crashes. Use-after-free, double-free, buffer overflows — the bugs behind "Bun crashed and I don't know why" reports — are the exact class that Rust's borrow checker prevents at compile time.

Later: faster feature velocity from the team. Less time on memory debugging means more time shipping. Better profiling (perf), fuzzing (cargo-fuzz), and static analysis (clippy, miri) come with the Rust ecosystem.

# Ecosystem Implications

# Zig

Bun was the highest-profile Zig project in production. Losing it suggests that missing compile-time safety guarantees is a real cost at this scale — not a theoretical one.

That said, 32% of Bun is still Zig. Its strengths (simple mental model, C interop, comptime) haven't gone away. This is about bug surface area at Bun's scale, not about Zig being a bad language.

# Rust for JS Runtimes

Node.js: C++. Deno: Rust. Bun: now Rust. The pattern is consistent — memory safety without GC, strong concurrency guarantees, good C/C++ interop for engine integration.

# AI-Driven Porting

The workflow across all 5 PRs was the same loop:

  1. Claude generates bulk translations (Zig semantics → Rust)
  2. CI runs the test suite on all platforms
  3. Claude does adversarial review
  4. Jarred approves architecture-level decisions
  5. Claude fixes what breaks

With a comprehensive test suite, an AI can translate a codebase and the tests prove it works. The follow-up PRs show the same loop applied to performance and code quality work.

# Build System Changes

The migration touches the full build pipeline:

  • Build tool: bun run build compiles the Rust workspace via Cargo + CMake for C++ components
  • Toolchain: Rust nightly (pinned in rust-toolchain.toml) + LLVM 21 for C++
  • Code generation: TypeScript scripts generate Rust ↔ C++ FFI bindings
  • Linting: clippy with 45 deny lints enforced in CI
  • Formatting: cargo fmt --all replaces zig fmt
  • Sanitizers: AddressSanitizer covers Rust code, C++ bindings, and all dependencies in debug builds
  • Incremental builds: Cargo's incremental compilation makes Rust-only rebuilds fast; only the final link step is unavoidable

For contributors, the workflow is now:

bash
# Type-check without linking (fast feedback)
cargo check -p <crate>

# Or check entire workspace
bun run rust:check

# Watch mode — runs cargo check on every save
bun run watch

# Community Reaction

PR #30412's GitHub reaction ratio:

  • 👍 1,607 (50.6%)
  • 👎 1,559 (49.4%)
  • 😄 577

A near-even split with 1,384+ comments — an unusually divisive PR. The follow-up PRs (#30875, #30901, #31115, #31116) received no controversy.

# Bottom Line

1.03M lines across 5 PRs in 11 days. All on claude/ branches at 646 commits/day. Bundler went from 12% slower to roughly matching the Zig baseline. Memory leaks fixed across every native subsystem. 2,735 clippy violations cleaned up, 6,200 lines of dead code removed.

The test suite passes on all platforms, the binary is smaller, and the Rust toolchain now catches at compile time what used to be weeks of debugging.

Try it: bun upgrade --canary. Stable release is waiting on a ~250ms main-thread gap on 100k-module bundles — five functions each under 2% CPU, mostly cache-miss-bound.

Frequently Asked Questions

Why did Bun switch from Zig to Rust?
The primary motivation was memory safety tooling. Bun's team spent enormous development and debugging time tracking memory bugs that Rust's borrow checker catches at compile time. The switch gives them compiler-assisted prevention of use-after-free, double-free, and data race bugs.
Is Bun faster after the Rust rewrite?
Yes. After follow-up optimisation PRs, the Rust bundler is 1.05× faster than the Zig v1.3.14 baseline on a 10,000-module benchmark (3,443ms vs 3,398ms wall time). Total CPU usage is 41% lower than Zig due to better parallelism. The initial port was 1.10× slower, but the gap was closed within 3 days via targeted hot-path fixes.
Does Bun still use Zig after the rewrite?
Yes. Zig still makes up about 32% of the codebase. The rewrite replaced the majority of Zig code with Rust (now 46.6%), but Zig remains for certain components. C++ (13.2%) is still used for the JavaScriptCore bridge layer.
Is the Rust rewrite available in stable Bun?
Not yet as of May 2026. The Rust rewrite is available in the canary channel (bun upgrade --canary). The remaining work is a ~250ms main-thread gap on 100k-module bundles, spread across five functions each under 2% CPU time. The hard problems — memory leaks, bundler perf parity, code quality — are solved.
How much of the Rust rewrite was written by AI?
100% of the Bun Rust rewrite branches are prefixed `claude/`, indicating Anthropic's Claude model was the primary code author. Across 5 PRs totalling 7,111 commits and 1.03M lines added, the workflow was AI-generated code validated by CI and reviewed by Jarred Sumner. This includes the initial port, memory leak fixes, performance optimisations, and code quality enforcement.

Enjoyed this article?

Get notified when I publish new articles on automation, ecommerce, and data engineering.

bun rust rewritebun rewrite zig to rustbun rust migrationbun programming language changebun rust performancejavascript runtime rustbun oven sh rustbun memory safetybun rust reviewzig vs rust bun