needhelp
← Volver al blog

La reescritura de Bun en Rust: 11 días, 6.778 commits y lo que de verdad cambió

por needhelp
Bun
Rust
Zig
JavaScript Runtime
LLM
Software Engineering

El equipo de Bun hizo algo poco habitual en mayo de 2026: reemplazó todo el núcleo nativo del runtime por un port en Rust, y lo hizo en 11 días.

La advertencia al principio del anuncio marca el tono. Anthropic adquirió Bun en diciembre de 2025. Jarred Sumner, creador de Bun, usó una versión preliminar de Claude — un “modelo de clase Mythos” al que llaman Fable 5 — para gran parte del trabajo. No es un experimento teórico. Bun v1.4.0, la primera versión en Rust, ya está en canary.

Las cifras en crudo:

  • 535.496 líneas de Zig (sin contar comentarios) se convirtieron en un código base en Rust
  • 6.778 commits entre el 3 y el 14 de mayo
  • 1.448 archivos .zig portados mecánicamente a .rs
  • ~$165.000 en gasto de API (5,9B de tokens de entrada sin caché, 690M de tokens de salida)
  • Pico: 64 Claudes corriendo a la vez repartidos en 4 worktrees

Arranque de Bun tras la reescritura en Rust, ejecutándose bajo Claude Code

Bun siempre fue una apuesta por Zig

Bun empezó como un port línea por línea del transpilador JS/TS de esbuild, de Go a Zig. Sumner escribió su primera línea de Zig el 16 de abril de 2021, tras ver la referencia del lenguaje Zig de una sola página en Hacker News. La versión inicial — transpilador, minificador, empaquetador, gestor de paquetes compatible con npm, test runner al estilo de Jest, superficie de API de Node.js — la escribió una sola persona en un año, en un apartamento pequeño de Oakland, antes de que los LLM sirvieran para este tipo de trabajo.

La apuesta salió bien. La CLI de Bun supera ahora los 22 millones de descargas mensuales. Claude Code y OpenCode la usan como runtime. Vercel, Railway y DigitalOcean ofrecen soporte oficial de primera clase.

El alcance también era el problema.

La deuda: memoria con GC junto a memoria manual

JavaScript usa recolección de basura. Bun integra JavaScriptCore (el motor de Safari) más un montón de librerías en C/C++: uWebSockets, BoringSSL, SQLite, lsquic. Zig, como C, no gestiona la memoria por ti. El trabajo de Bun es estar entre un lenguaje con GC y código nativo de gestión manual, y ese límite es donde vivían la mayoría de sus peores bugs.

El anuncio lista una muestra de lo que arreglaron solo en v1.3.14: un heap-use-after-free en node:zlib, un use-after-free en node:http2 por callbacks reentrantes, escrituras fuera de rango en UDPSocket.sendMany, una fuga de memoria por cada tls.connect, un double-free en el parser de CSS. La lista sigue, y es técnica.

Sumner deja claro que no es culpa de Zig. “We wouldn’t have gotten this far if not for Zig, and I’ll always be grateful.” El problema es estructural: gestionar los lifetimes a través de un límite con GC es difícil en cualquier lenguaje que no esté diseñado para ello, y la respuesta de Zig — un defer explícito en cada punto de llamada — se presta a confundirse en rutas de error que rara vez se alcanzan.

Valoró C++. Cerca del 20% de Bun ya es C++, y mover más daría constructores y destructores. Pero seguiría dependiendo de guías de estilo impuestas por revisión de código, y la corrupción de memoria seguiría ocurriendo.

La propuesta de Rust es distinta: en Rust seguro, el use-after-free, el double-free y el “se te olvidó liberar” son errores de compilación. El borrow checker y Drop convierten un problema de guía de estilo en un problema del sistema de tipos.

La estrategia: transpilar, no rediseñar

Las reescrituras tienen mala fama, y con razón. Una reescritura desde cero de 535K líneas habría congelado funciones y correcciones durante un año. La forma que eligió Bun fue un port mecánico: la misma arquitectura, los mismos objetivos de rendimiento, las mismas funciones, la misma suite de tests — solo en Rust.

Dos decisiones sostuvieron todo el esfuerzo:

  1. Todo de una vez, no de forma incremental. La experiencia de Sumner portando esbuild a Zig le dijo que las reescrituras incrementales dejan andamiaje temporal que molesta más de lo que ayuda.
  2. Que parezca transpilado. El objetivo era un Rust que se lea como el Zig del que venía, para refactorizarlo hacia un Rust idiomático después de soltar v1.4.

Lo crucial: la suite de tests de Bun está escrita en TypeScript, así que no le importa en qué lenguaje esté implementado el runtime. Eso les permitió tratar “todos los tests en verde” como la definición de terminado.

La preparación fue pequeña pero deliberada. Antes de escribir código, Sumner pasó ~3 horas con Claude mapeando patrones de Zig a Rust; esa conversación se convirtió en PORTING.md (luego apareció en Hacker News). Una segunda pasada analizó los lifetimes correctos en Rust de cada campo de cada struct del código base y los serializó en LIFETIMES.tsv. Ambos documentos pasaron por una revisión adversarial antes de portar una sola línea.

Lo que se quedó quieto: JavaScriptCore y las librerías embebidas en C/C++. La reescritura era el pegamento nativo y el código propio de Bun, no el motor de JS.

Ejecución: 64 Claudes, 11 días

El port corrió como unos 50 “dynamic workflows” en Claude Code, de forma continua, durante 11 días. Cada workflow era un bucle: coger una tarea, producir código, que lo revisaran, aplicar la retroalimentación.

El modelo de revisión es la parte interesante. Por cada implementador, había dos o más revisores adversariales — sesiones separadas de Claude a las que se les decía que asumieran que el código está mal y que encontraran cada razón por la que falla. El implementador nunca revisa; el revisor nunca implementa. El anuncio lo plantea como un reflejo de la revisión humana: el autor quiere hacer merge, así que lo comprueba una parte distinta.

Vale la pena sacar algunos mecanismos:

  • Ensayo primero. Portaron 3 archivos antes de comprometerse con los 1.448.
  • Sharding por worktree. Las primeras corridas tenían Claudes pisándose unos a otros con git stash y git reset. La solución: 4 worktrees, 16 Claudes cada uno, con la regla de no correr git ni cargo a mitad de tarea.
  • Errores del compilador como cola de trabajo. cargo check volcó ~16.000 errores a un archivo, agrupados por crate, y 64 Claudes fueron limando — 16 bucles de fix/review/apply repartidos en 4 worktrees.
  • Dependencias cíclicas. Zig era efectivamente una sola unidad de compilación; Rust necesitaba ~100 crates. Desenredar los ciclos sacó a la luz la mayoría de esos 16.000 errores.
  • Aislamiento. Los tests de estrés que agotan los sockets TCP o lanzan ~10k procesos corrían bajo cgroups de systemd-run. La máquina se quedó sin disco y se cayó un par de veces.
timeline
    title La reescritura de Zig → Rust de Bun en 11 días (mayo de 2026)
    2026-05-03 : Rama de port abierta (PR #30412)
    2026-05-04 : Primer lote de borrador de 100 archivos
    2026-05-06 : ~16.000 errores de compilador, 64 Claudes
    2026-05-08 : Primera corrida de CI (972 archivos fallando)
    2026-05-09 : Linux x64 en verde
    2026-05-11 : Windows en verde (última plataforma)
    2026-05-14 : Las 6 plataformas en verde, mergeado

Por las cifras: pico de 1.300 líneas de código por minuto, 695 commits en la hora más intensa (6 de mayo), 58 commits en el minuto más intenso. Cada commit fue revisado por dos revisores adversariales antes de entrar. El diff final fue de +1.009.272 líneas. No se saltó ni se borró ningún test.

CI es donde se volvió real. Dos días después de la primera corrida, los archivos de test fallando bajaron de 972 a 23. Un día y medio más tarde Linux estaba totalmente en verde. Windows terminó de último el 11 de mayo; las seis plataformas estaban en verde en el build #54202, el 14 de mayo.

Lo que Rust de verdad aportó

El objetivo declarado era la estabilidad, y el post lo respalda con cifras duras.

Bugs. Bun v1.4.0 arregla 128 bugs que se reproducen en v1.3.14.

Memoria. Drop reemplazó el defer por punto de llamada. El ejemplo del post: empaquetar el mismo proyecto de 60 módulos 2.000 veces en un solo proceso. En v1.3.14 cada build filtra ~3 MB para siempre; en v1.4.0 la memoria se estabiliza.

Builds v1.3.14 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

Un intento previo de arreglar esto en Zig nunca se mergeó, escribe Sumner, porque la falta de un equivalente a Drop dificultaba tener confianza.

Tamaño del binario. Solo la reescritura en Rust recortó 3,8 MB (Windows), 5,5 MB (macOS), 6,8 MB (Linux) — sobre todo por dejar de usar tanto comptime de Zig. Más trabajo de linker (identical code folding, recorte de datos de ICU) llevó la reducción total a ~20% en Linux y Windows.

Versión Plataforma Tamaño
v1.4.0 Windows 76 MB
v1.3.14 Windows 94 MB
v1.4.0 Linux 70 MB
v1.3.14 Linux 88 MB

Velocidad. El LTO entre lenguajes de C/C++ y Rust deja que el compilador haga inline a través de la frontera de lenguajes. Bun midió ganancias del 2–5%:

  • Bun.serve: 169,6k → 177,7k req/s (+4,8%)
  • express: 64,5k → 66,6k (+3,2%)
  • next build: 13,62s → 13,03s (+4,5%)
  • tsc -b --force: 0,94s → 0,89s (+4,7%)

Prisma movió su beta pública de Compute al port en Rust. Alexey Orlenko: “We ran into memory leaks and a connection pool that couldn’t recover after a VM was paused and resumed. When the Rust rewrite appeared, we tested it against the same failure modes. It handled them perfectly.”

Claude Code v2.1.181 (17 de junio) y posteriores usan el port en Rust. El arranque es un 10% más rápido en Linux. El veredicto de Sumner sobre el cambio visible para el usuario: “Boring is good.”

Donde el port falló

Un transpile fiel de 535K líneas no es gratis. El post documenta 19 regresiones conocidas, todas arregladas. Las que enseñan son pequeños huecos semánticos entre lenguajes:

  • Efectos secundarios de debug_assert!. El assert de Zig es una función, así que su argumento se ejecuta en cada build. El debug_assert! de Rust es una macro que se borra en release — una llamada a insert_stale que mutaba el estado de HMR dejó de correr en silencio. (#30678)
  • Slices de longitud impar. El helper de Zig ignoraba un byte impar final; bytemuck::cast_slice hace panic con eso. Blob.text() sobre un BOM UTF-16 más un conteo de bytes impar empezó a crashear. (#31188)
  • Comprobaciones de límites. El ReleaseFast de Zig las quita; Rust las mantiene. Una constante placeholder bajó el techo de interning de nombres de archivo de 8,4M a 270K, y proyectos reales lo golpearon. (#31503)
  • Cadenas de formato comptime. Zig evalúa las cadenas de formato en tiempo de compilación, así que los marcadores de color desaparecen antes de sustituir los argumentos. Rust no tiene comptime, así que el parser de marcadores se comió una barra invertida literal en los hipervínculos OSC 8. (#30693)

Estos son los bugs que solo salen porque dos lenguajes se ven idénticos pero no lo son.

Qué significa esto

Quitando el ángulo del LLM, hay una historia de ingeniería real: un proyecto topó con el techo de lo que la gestión manual de memoria podía darle a gran escala, y eligió el borrow checker de Rust como la herramienta para hacer que toda una clase de bugs fuera imposible en vez de solo desaconsejada.

El ángulo del LLM es la parte que la gente discutirá. Sumner es franco en que esto le habría tomado a tres ingenieros con contexto completo del código base cerca de un año, y que nunca lo habrían hecho — la alternativa realista era “no hacer nada y seguir arreglando bugs”. En cambio, un ingeniero supervisando 64 Claudes lo hizo en 11 días por ~$165K.

Cerca del 4% del código Rust de Bun está en bloques unsafe (~13.000 palabras clave unsafe en ~27.000 líneas de ~780.000 totales), y el 78% de esos bloques son de una sola línea. Desde el merge, han corrido 11 rondas de revisión de seguridad y añadido fuzzing guiado por cobertura 24/7 en todos los parsers — 100 mil millones de ejecuciones hasta ahora, ~15 PRs.

La pregunta de mantenibilidad es la que yo vigilaría. Sumner dice que el Rust se lee como el Zig, y muestra un lado a lado de canMergeSymbols para ilustrarlo. La apuesta es que un port fiel es revisable port a port, y eso es lo que permitió que una persona diera el visto bueno a un diff de un millón de líneas.

Su línea final es la que envejecerá de forma interesante: “One engineer can do a lot more today than a year ago.”

Referencias

Compartir esta página