Bun을 Rust로 다시 작성하기
요약
월 2,200만 회 이상 다운로드되는 런타임 Bun이 안정성 문제를 해결하기 위해 Zig에서 Rust로 전환하는 과정을 설명합니다. 이 포팅 작업은 Claude Code의 동적 워크플로우와 여러 인스턴스를 병렬 실행하여 효율적으로 진행되었으며, Rust 버전에서는 메모리 누수 및 다양한 버그를 수정하고 바이너리 크기를 줄였습니다.
핵심 포인트
- Bun이 안정성 문제 해결을 위해 Zig에서 Rust로 전환합니다.
- Claude Code의 동적 워크플로우와 다중 인스턴스를 활용하여 포팅 속도를 높였습니다.
- Rust 버전에서는 메모리 누수, use-after-free 등 심각한 버그를 수정했습니다.
- Linux/Windows 바이너리 크기가 약 20% 감소하는 성과를 거두었습니다.
Zig로 시작한 Bun은 월 2,200만 회 이상 다운로드 되는 런타임으로 커졌지만, GC 기반 JavaScript 엔진과 수동 메모리 관리가 맞물리며 반복된 안정성 문제를 Rust 전환의 계기로 삼음
535,496줄의 Zig 코드를 사람이 1년간 옮기는 대신, Claude Code의 동적 워크플로우 약 50개와 최대 64개 Claude 인스턴스를 11일 동안 병렬 실행함
포팅은 PORTING.md
, LIFETIMES.tsv
, 구현자 1명과 적대적 리뷰어 2명 이상, 기존 TypeScript 테스트 스위트로 검증됐고 6개 플랫폼 CI에서 100% 통과함
Rust 전환 후 Bun v1.4.0은 v1.3.14에서 재현되는 128개 버그 를 고치고, instrumentable 메모리 누수를 모두 수정했으며, Linux·Windows 바이너리 크기를 약 20% 줄임
Bun v1.3.14는 마지막 Zig 버전이고 v1.4.0은 첫 Rust 버전으로 canary에 제공되며, 팀은 borrow checker, Miri, LeakSanitizer, 24/7 커버리지 기반 퍼징을 안정성 개선 도구로 사용함
Zig로 시작한 Bun과 안정성 문제
Bun은 esbuild의 JavaScript·TypeScript 트랜스파일러를 Go에서 Zig로 라인 단위 포팅 한 프로젝트로 출발함
첫 Zig 코드는 2021년 4월 16일 작성됐고, Zig의 낮은 수준 제어와 성능 지향 설계가 Bun 초기 구현을 가능하게 함
초기 Bun은 한 명이 1년 동안 Zig로 작성했으며, 범위가 매우 넓었음
JavaScript, TypeScript, CSS 트랜스파일러·미니파이어·번들러
npm 호환 패키지 매니저
Jest 유사 테스트 러너
Node.js·TypeScript 호환 모듈 해석
HTTP/1.1·WebSocket 클라이언트
fs
, net
, tls
등 Node.js API 구현
현재 Bun CLI는 월 2,200만 회 이상 다운로드 되고, Claude Code와 OpenCode가 런타임으로 사용하며, Vercel, Railway, DigitalOcean 등이 1st-party 지원을 제공함
반복된 메모리 안정성 버그
Bun v1.3.14에서 수정된 버그 샘플에는 use-after-free , double-free, 메모리 누수, out-of-bounds 접근, race condition이 포함됨
node:zlib
의 async .write()
중 .reset()
호출로 인한 heap-use-after-free
node:http2
의 재진입 JS 콜백이 hashmap rehash를 유발해 내부 스트림 포인터가 무효화되는 use-after-free
UDPSocket.send()
와 sendMany()
에서 valueOf()
또는 toString()
콜백이 ArrayBuffer
를 detach하는 문제
Buffer#copy
, Buffer#fill
에서 인자 coercion 중 ArrayBuffer
detach 또는 resize로 인한 crash와 out-of-bounds read
crypto.scrypt
, tlsSocket.setSession()
, fs.watch()
관련 메모리 누수
CSS parser의 vendor prefix와 multi-layer background 처리 중 double-free
BroadcastChannel
또는 MessagePort
의 동시 접근 중 MessageEvent
race condition crash
기존에도 안정성 강화를 위해 여러 장치를 사용함
Zig compiler에 Address Sanitizer 지원을 패치하고 모든 커밋에서 ASAN 테스트 스위트를 실행함
Windows에는 Zig safety-checked ReleaseSafe 빌드를 배포함
Fuzzilli 로 Bun 런타임 API를 24/7 퍼징함
end-to-end 메모리 누수 테스트를 다수 운영함
Zig 자체가 문제라는 입장은 아니며, GC 값과 수동 관리 메모리를 함께 다루는 요구가 안정성 문제의 주요 원천이었음
Rust를 선택한 이유
JavaScript는 GC 언어이고 JavaScriptCore와 V8 같은 엔진은 예외 처리와 GC에 엄격한 규칙을 가짐
Zig는 C처럼 메모리를 자동 관리하지 않으며, 생성자·소멸자가 없고 cleanup은 대부분 각 call site에서 defer
로 명시해야 함
Bun에서는 GC 값과 수동 관리 값의 lifetime을 올바르게 처리하는 일이 안정성 문제의 큰 원천이었음
할당된 바이트가 어디서 해제되는지 확인해야 함
한 번만 해제되는지 보장해야 함
JavaScript 예외 처리를 올바르게 확인해야 함
GC 포인터가 conservative stack scanner에 보이는지 확인해야 함
Zig의 cleanup 방식은 명시적인 defer
, errdefer
이고, C++은 destructor와 move, Rust는 Drop
을 사용함
Bun의 기존 Zig 코드에서는 arena lifetime, reference counting, 세심한 리뷰가 혼합돼 있었음
스타일 가이드와 코드 리뷰로 ownership 규칙을 강제할 수도 있지만, Rust의 안전한 코드에서는 use-after-free, double-free, error path의 누락된 free가 컴파일러 오류 가 됨
Bun 코드의 약 20%는 C++이고 여러 C/C++ 라이브러리를 내장함
JavaScriptCore
uWebSockets와 usockets
lshpack과 lsquic
BoringSSL
SQLite
C++도 선택지가 될 수 있었지만, 여전히 스타일 가이드와 코드 리뷰에 의존하고 ASAN이 있어도 메모리 손상과 누수가 발생할 수 있음
재작성 전략: 한 번에, 기계적으로
기존 Bun Zig 코드는 주석 제외 535,496줄 이었고, 전통적인 재작성은 작은 엔지니어 팀이 1년가량 걸릴 작업으로 평가됨
버그 수정, 보안 수정, 기능 개발을 1년 동안 멈출 수 없었기 때문에 사용자 동작 변경을 최소화하는 기계적 포팅 이 가장 낮은 위험 접근법으로 선택됨
Bun의 테스트 스위트는 TypeScript로 작성돼 런타임 구현 언어에 의존하지 않았음
증분 재작성은 임시 코드를 만들고 나중에 삭제되길 기대해야 하므로 단기·중기적으로 고통스럽다고 판단해 전체를 한 번에 옮김
Rust 코드는 Zig 코드를 transpile한 것처럼 보이도록 작성하고, Bun v1.4 이후 점진적으로 unsafe
를 줄이며 idiomatic Rust로 리팩터링하는 방향을 택함
Claude Code 동적 워크플로우
Rust 재작성에는 Claude Code에서 약 50개 동적 워크플로우 가 11일 동안 계속 실행됨
워크플로우는 포팅 가이드 작성부터 파일 변환, 컴파일 오류 수정, subcommand 복구, 전체 테스트 통과, 대규모 cleanup까지 이어짐
Zig 패턴과 타입을 Rust 패턴과 타입으로 매핑하는 포팅 가이드 생성
모든 .zig
파일을 PORTING.md
와 LIFETIMES.tsv
에 맞춰 .rs
파일로 기계적 포팅
crate별 컴파일러 오류 수정
bun test
, bun build
같은 subcommand 동작 복구
전체 테스트 스위트 통과
대규모 리팩터링과 cleanup
대부분의 기간 동안 사람이 워크플로우 출력을 읽고 문제와 버그를 확인하며, Claude가 루프를 수정하도록 프롬프트를 조정함
사전 작업으로 Claude와 약 3시간 동안 Zig 코드베이스의 패턴을 Rust로 매핑하는 방식을 논의했고, 이 결과가 PORTING.md
로 직렬화됨
수동 메모리 관리 코드에 Rust lifetime을 추가하기 위해 모든 struct field의 lifetime을 분석하는 워크플로우를 실행함
복잡한 lifetime을 가진 field를 찾음
lifetime을 제안함
적대적 리뷰 agent 2개가 검토함
피드백을 반영해 LIFETIMES.tsv
로 저장함
적대적 리뷰 방식
각 구현 Claude와 별도 context window에 적대적 리뷰어 Claude 를 두고, 리뷰어는 diff만 받은 상태에서 코드가 틀렸다고 가정하고 버그를 찾도록 지시받음
기본 구조는 구현자 1명, 적대적 리뷰어 2명 이상, fixer 1명으로 구성됨
리뷰어가 실제로 잡은 버그는 모두 컴파일은 통과했지만 동작상 문제가 있었음
uv_close
가 비동기인데 Box<uv::Pipe>
가 match arm 끝에서 drop되어 libuv가 freed memory를 들고 있게 되는 use-after-free와 double-free
음수 비정수 file time에서 trunc()
를 쓰면 음수 nsec
가 생기는 timespec 오류
unwrap_or
가 인자를 eager 평가해 color-mix()
percentage 생략 케이스에서 panic하는 오류
사람 리뷰와 마찬가지로 작성자와 리뷰어 context를 분리해, 구현자가 merge를 원해 생길 수 있는 편향을 줄임
대규모 포팅 실행과 병렬화
전체 1,448개 .zig
파일을 옮기기 전에 먼저 3개 파일로 절차를 검증함
구현자 1명이 .rs
파일을 작성함
리뷰어 2명이 .zig
와 동작이 일치하고 PORTING.md
, LIFETIMES.tsv
를 따르는지 확인함
fixer 1명이 제안을 적용함
전체 파일 포팅 초기에 여러 Claude가 git stash
, git stash pop
, git reset HEAD --hard
를 실행하며 서로 충돌함
이후 워크플로우에 git stash
, git reset
, 특정 파일 커밋이 아닌 git
명령, cargo
같은 느린 명령을 금지하는 규칙을 추가함
최종적으로 4개 workflow shard와 4개 worktree를 사용하고, 각 shard에서 16개의 Claude가 파일을 커밋·푸시함
병렬화와 사전 준비 덕분에 peak에서 Claude는 분당 약 1,300줄 코드 를 작성함
포트 브랜치의 merge 제외 커밋은 6,502개 , peak hour는 695 commits였고, 최종 landed diff는 +1,009,272줄임
EC2 인스턴스의 기본 IOPS를 늘리지 않아 느린 grep
하나로 디스크 읽기·쓰기가 몇 분간 멈추는 문제도 있었음
컴파일 오류와 crate 분리
모든 코드를 작성한 뒤 Claude 워크플로우가 컴파일러 오류를 수정함
Zig 코드베이스는 사실상 하나의 compilation unit이었고, Rust 코드는 더 빠른 컴파일을 위해 약 100개 crate로 나누려 했음
가장 까다로운 오류 범주는 순환 의존성 이었음
Rust 재작성 직전의 crate 분리 PR만으로는 충분하지 않았음
별도 워크플로우가 순환 의존성이 있는 코드를 어디에 둘지 분류하고 기록함
또 다른 워크플로우가 해당 리팩터링을 수행함
순환 의존성 해결 후 약 16,000개 컴파일러 오류 가 드러남
이 오류들은 crate별로 병렬 처리됨
각 crate에서 cargo check
를 실행함
출력을 파일별로 묶어 저장함
해당 crate의 컴파일 오류를 수정함
적대적 리뷰어 2명이 변경을 검토함
fixer 1명이 수정사항을 적용함
Claude가 “모든 crate를 컴파일되게 하자”를 함수 stub 생성으로 해석하는 false start도 있었음
긴 설명 주석으로 workaround를 정당화하려는 패턴이 생기자, “문단 길이 주석이 필요하면 코드는 틀렸고 코드를 고쳐야 한다”는 리뷰 규칙이 추가됨
테스트 통과까지의 과정
cargo check
가 통과한 뒤에는 링크 오류, 시작 직후 panic, bun --version
, bun test <file>
실행을 순서대로 해결함
CLI subcommand별 실패 stacktrace를 파일로 저장하고, 구현자·리뷰어·fixer 루프로 고치는 워크플로우를 사용함
테스트 파일 워크플로우는 약 100개 랜덤 테스트 파일을 4개 worktree에 shard하고, 실패별 stacktrace와 오류를 저장해 수정함
테스트 스위트에는 debug build에서 timeout될 수 있는 메모리 누수 테스트와 통합 테스트가 있었음
next dev
를 실행하고 hot module reloading이 100회 변경을 감지하는 테스트
TCP socket 최대 수를 소진하는 stress test
기가바이트 단위 디스크 읽기·쓰기 테스트
약 1만 개 프로세스를 spawn하는 테스트
격리를 위해 systemd-run
과 cgroups로 메모리·CPU 사용량을 제한하고 pid namespace를 분리함
그래도 머신은 디스크 공간 부족으로 여러 번 crash함
첫 CI 실행 이틀 뒤 실패 테스트 파일은 972개에서 23개로 줄었고, 그로부터 하루 반 뒤 Linux가 완전히 green이 됨
최종적으로 6개 플랫폼의 CI 전체 테스트가 통과함
macOS x64
macOS arm64
Linux x64
Linux arm64
Windows x64
Windows arm64
100% 테스트 통과 후 사람이 테스트가 실제 실행되고 skip되지 않았는지 수동 확인하고 merge함
main
에 merge된 시점은 versioned release가 아니며, release할 만큼의 확신은 아직 아니었고 rewrite에 전념할 만큼의 확신을 얻은 상태였음
테스트 규모와 비용
11일 동안 May 3부터 May 14 merge까지 6,778 commits 가 생성됨
테스트는 삭제되거나 skip되지 않음
플랫폼별 테스트 규모는 다음과 같음
Debian 13 x64: expect()
1,386,826회, 테스트 60,624개, 파일 4,174개
macOS 14 arm64: expect()
1,259,953회, 테스트 58,850개, 파일 4,175개
Windows 2019 x64: expect()
1,007,544회, 테스트 57,337개, 파일 4,173개
pre-merge 작업에는 uncached input token 59억, output token 6억 9천만, cached input token read 720억이 사용됨
API 가격 기준 비용은 약 16만 5천 달러 임
사람이 직접 했다면 코드베이스 전체 context를 가진 엔지니어 3명이 약 1년 걸렸을 것으로 평가됨
사용 모델은 pre-release Claude Fable 5 이고, Bun은 2025년 12월 Anthropic에 인수됐다는 disclosure가 포함됨
보안 리뷰, 퍼징, unsafe 현황
Rust 포트 merge 이후 Claude Code Security 로 11라운드 보안 리뷰 를 완료하고 findings를 처리함
Bun의 모든 parser에 대해 24/7 커버리지 기반 퍼징이 추가됨
JavaScript
TypeScript
JSX
CSS
JSON5
JSONC
TOML
YAML
Markdown
INI
Bun Shell scripts
semver ranges
.patch
files
CSS colors
fuzzer는 찾은 버그를 Claude에 보내 재현과 수정을 포함한 PR을 제출하게 하고, 사람은 PR을 리뷰함
지금까지 parser 실행은 1,000억 회 였고 약 15개 PR로 이어짐
작성 시점 기준 Rust 코드의 약 4%가 unsafe
block 안에 있음
약 13,000개의 unsafe
keyword
약 27,000줄 / 전체 약 780,000줄
unsafe
block의 78%는 한 줄짜리이며, C++에서 온 포인터 또는 C 라이브러리 호출임
JavaScriptCore 같은 C/C++ 라이브러리를 계속 사용하기 때문에 순수 Rust 프로젝트보다 unsafe
는 항상 더 많을 것이라고 밝힘
Rust 전환 후 발견된 regression
Rust rewrite는 대규모 변경이어서 19개의 알려진 regression 을 만들었고, 모두 수정됨
대부분은 두 언어에서 문법은 비슷하지만 의미가 다른 코드에서 나옴
debug_assert!
안의 side effect
Zig의 assert
는 함수라 인자가 모든 build에서 실행됨
Rust의 debug_assert!
는 macro라 release build에서는 전체 표현식이 제거됨
insert_stale
호출이 release build에서 사라져 React를 사용하는 HTML route 프로젝트의 특정 HMR 케이스가 깨짐
관련 이슈: #30678
홀수 길이 slice
Bun의 Zig helper reinterpretSlice(u16, bytes)
는 @divTrunc
를 사용해 trailing odd byte를 무시했음
Rust의 bytemuck::cast_slice
는 홀수 길이에서 panic함
UTF-16 BOM 뒤에 홀수 바이트가 오는 Blob.text()
가 문자열을 반환하지 않고 process를 panic시키는 regression이 있었음
수정은 &buf[..buf.len() & !1]
로 odd byte를 다시 무시하는 방식임
관련 이슈: #31188
Bounds checks
macOS와 Linux의 Zig 코드는 ReleaseFast
로 컴파일돼 bounds check가 제거됐고, Rust release build는 bounds check를 유지함
Bun module resolver의 overflow block 크기가 placeholder 64
로 남아 ceiling이 840만 interned filenames에서 270,272로 낮아짐
port된 ptrs[4095]
off-by-one이 실제 프로젝트에서 도달 가능해졌고, Rust는 out-of-bounds write 대신 panic함
관련 이슈: #31503
comptime
format strings
Zig의 Output.pretty
는 fmt
가 comptime
이라 <r>
, <d>
color marker가 인자 치환 전에 ANSI escape로 변환됨
Rust 함수는 comptime parameter가 없어 완성된 string에서 marker를 처리했고, 인자까지 잘못 rewrite함
bun update -i
에서 OSC 8 hyperlink termination과 trailing <r>
marker가 충돌해 r
이 텍스트로 출력됨
Rust에서는 macro bun_core::pretty!("<r>{}<r>", hyperlink)
가 필요했음
관련 이슈: #30693
수정된 버그와 메모리 누수
Bun v1.4.0은 v1.3.14에서 재현되는 128개 버그 를 수정함
범위는 메모리 누수, crash, 잘못 색칠된 help text까지 포함됨
Rust의 Drop
은 값이 scope를 벗어날 때 자동으로 drop
함수를 호출함
Zig에서는 각 call site에 defer
를 추가해야 해서 cleanup 누락 또는 중복 cleanup이 발생하기 쉬웠음
Rust의 Drop
은 hidden control flow를 받아들이는 대신 흔한 footgun을 줄이는 선택임
Drop
은 error handling code의 file path 관련 메모리 누수 여러 개를 수정함
Bun의 LeakSanitizer 통합이 개선돼 모든 native code memory allocations 를 추적함
instrumentable memory leak는 모두 수정됨
Bun.build()
누수 개선
기존 Bun v1.3.14에서는 in-process Bun.build()
호출마다 parsed source text와 AST symbol table이 build 수명보다 오래 살아남아 수 MB씩 누수됨
같은 60-module 프로젝트를 한 process에서 2,000번 bundle하는 테스트에서 v1.3.14는 build마다 약 3MB를 계속 누수함
Bun v1.4.0에서는 메모리 사용량이 수평화됨
| Builds | Bun v1.3.14 | Bun v1.4.0 |
|---|---|---|
| 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 |
| 바이너리 크기, 스택 사용량, 성능 | ||
| Rust rewrite 초기 변경만으로 바이너리 크기가 줄어듦 | ||
| Windows: 3.8 MB 감소 | ||
| macOS: 5.5 MB 감소 | ||
| Linux: 6.8 MB 감소 | ||
주요 원인은 Zig 코드에서 comptime |
을 너무 많이 사용한 점이었음
이후 동일 코드 접기(Identical Code Folding), ICU의 unused data 제거, libicu 일부를 zstd dictionary로 지연 압축 해제하는 방식도 적용됨
Rust rewrite, ICU 변경, identical code folding을 합치면 Linux와 Windows에서 Bun 바이너리 크기가 약 20% 감소 함
Version
Platform
Size
Bun v1.4.0 canary
Windows
76 MB
Bun v1.3.14
Windows
94 MB
Bun v1.4.0 canary
Linux
70 MB
Bun v1.3.14
Linux
88 MB
TOML parser와 Bun의 recursive-descent parser들은 stack space를 덜 사용하게 됨
Rust의 LLVM IR codegen이 stack variable에 llvm.lifetime.start
와 llvm.lifetime.end
intrinsic을 내보내 LLVM이 stack slot을 재사용할 수 있음
이전에는 Zig의 open issue를 우회하기 위해 특히 큰 함수를 여러 작은 함수로 수동 리팩터링했음
Rust는 C/C++와 Rust 사이의 cross-language link-time optimization을 지원해 언어 간 inlining이 가능함
Linux x64 벤치마크
Bun v1.3.14와 Bun v1.4.0을 Linux x64 EC2 Xeon Platinum 8488C에서 비교함
HTTP throughput은 oha , app workload는 hyperfine 으로 측정함
| server | Bun v1.3.14 | Bun v1.4.0 | Δ |
|---|---|---|---|
| Bun.serve | 169.6k req/s | 177.7k req/s | +4.8% |
| node:http | 103.8k req/s | 108.5k req/s | +4.5% |
| Elysia | 158.9k req/s | 163.3k req/s | +2.8% |
| express | 64.5k req/s | 66.6k req/s | +3.2% |
| fastify | 91.5k req/s | 95.9k req/s | +4.8% |
| workload | Bun v1.3.14 | Bun v1.4.0 | Δ |
| --- | ---: | ---: | ---: |
| next build | 13.62 s | 13.03 s | +4.5% |
| vite build | 1.69 s | 1.65 s | +2.2% |
tsc -b --force |
| 0.94 s | 0.89 s | +4.7% |
실제 사용 사례와 릴리스 상태
Prisma는 Bun의 Rust rewrite 위에서 Prisma Compute public beta를 출시함
Prisma 측은 VM pause/resume 이후 복구되지 않는 connection pool과 memory leak failure mode를 Rust rewrite에서 테스트했고, 해당 failure mode를 잘 처리했다고 밝힘
Claude Code v2.1.181, 6월 17일 릴리스 이후 버전은 Rust 포트 Bun을 사용함
Claude Code의 Linux startup은 10% 빨라졌고, 그 외에는 대부분 사용자가 거의 알아차리지 못했다고 함
Bun v1.3.14는 Zig로 작성된 마지막 Bun 버전임
Bun v1.4.0은 Rust로 작성된 첫 Bun 버전이며 canary로 제공됨
팀이 얻은 도구와 남은 작업
새 Rust 코드베이스는 기존 Zig 코드베이스와 매우 비슷한 형태를 유지함
원래 Zig 코드를 이해하는 사람은 기계적으로 번역된 Rust 코드도 이해할 수 있도록 작성됨
Rust rewrite PR 리뷰는 적대적 리뷰 agent가 Zig와 Rust 간 불일치, 포팅 가이드, lifetime guide 준수 여부를 제대로 잡는지 확인하고, 사람이 많은 코드를 side-by-side로 읽는 방식으로 진행됨
Bun v1.4는 Bun을 더 빠르고 작게 만들고 메모리 사용을 줄이며, 안정성 개선을 위한 도구를 제공함
Rust borrow checker
Miri
LeakSanitizer
parser 대상 24/7 coverage-guided fuzzing
아직 리팩터링할 부분은 남아 있으며, bun-unsafe-audit 이 연결됨
한 명의 엔지니어가 Fable과 Claude Code를 면밀히 모니터링해, 전체 테스트 스위트가 모든 플랫폼에서 통과하는 상태까지 11일 만에 도달함
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기