13°
Portada del artículo: Bun se reescribió de Zig a Rust en once días. Medí lo que publicaron, y lo que no
RustArquitectura

Bun se reescribió de Zig a Rust en once días. Medí lo que publicaron, y lo que no

Bun portó 535.000 líneas de Zig a Rust y publicó tres cifras concretas: arranque, peticiones por segundo y tamaño de binario. La versión 1.3.14 fue la última en Zig y la 1.4.0 la primera en Rust, así que se descargan las dos y se mide lo mismo cambiando solo el binario. El arranque la supera con holgura, el binario se queda corto, y de memoria y CPU en reposo el anuncio no da ningún número: esas dos las mido sin nada oficial con qué contrastarlas.

Efrain Garay 27 de agosto de 2026

Reproduciendo el resumen

Un proyecto grande anunció que había reescrito su base de código entera en otro lenguaje, y publicó algunos números para demostrar que había salido bien: hasta un 20% menos de peso en el binario, hasta un 4,8% más de peticiones por segundo según el servidor, un 10% de arranque más rápido en una herramienta que corre sobre él. De memoria y de CPU en reposo no dijo nada.

La reescritura en sí me interesaba poco. Me quedé con otra cosa: las dos versiones se pueden descargar. La 1.3.14 fue la última construida en Zig y la 1.4.0 la primera construida en Rust, con el mismo servidor corriendo encima y el mismo sistema debajo. La única variable que cambia es el binario. Eso convierte un anuncio en algo comprobable, y comprobarlo cuesta una tarde.

Así que descargué las dos y medí lo que publicaron, y dos cosas más que no.

El banco corriendo de verdad: cuatro entornos de ejecución en contenedores con un núcleo y 512 MB cada uno, la carga alternando entre ellos, y las lecturas del cgroup en vivo. Narrado.Verlo en el visualizador de reels →

Qué es Bun y por qué esto es medible

Bun ejecuta JavaScript y TypeScript, el mismo papel de Node, dentro de un binario único que además trae gestor de paquetes, empaquetador y corredor de pruebas. Se vende como reemplazo directo de Node.

El proyecto describe la traducción como once días de trabajo intensivo sobre unas 535.000 líneas de Zig, y publica un desglose de cómo lo organizó. Ese proceso da para otra conversación entera. Lo que me interesa aquí es más estrecho y más comprobable: el binario resultante hace lo que dicen que hace.

Lo que Bun publica es esto: un 10% de arranque más rápido en Linux (medido en una herramienta que corre sobre Bun, no en Bun mismo), entre 2% y 5% más peticiones por segundo según el servidor, y cerca de un 20% menos de tamaño de binario. De memoria y de CPU en reposo no da ningún número, así que esas dos las mido sin nada con qué contrastarlas.

El montaje, y por qué el primer intento no servía

Las dos versiones van a directorios separados, sin tocar ninguna instalación que ya funcione:

for V in 1.3.14 1.4.0; do
  curl -fsSL https://bun.sh/install | BUN_INSTALL=$PWD/i-$V bash -s "bun-v$V"
  cp $PWD/i-$V/bin/bun $PWD/bin/bun-$V
done

El primer dato sale de ahí, sin ejecutar nada: 92,75 MB el binario en Zig, 80,76 MB el binario en Rust. Un 12,9% menos. El proyecto habla de cerca de un 20%, y la diferencia probablemente esté en qué se pesa exactamente; el número de arriba es el binario que baja de su instalador, medido con ls.

El servidor de prueba es el mismo para los tres candidatos, sin dependencias ni framework encima, porque cualquier librería sería una variable que no controlo:

const handler = () => new Response("hola", {headers: {"content-type": "text/plain"}});

if (typeof Bun !== "undefined") {
  Bun.serve({port: puerto, fetch: handler});
} else {
  require("http").createServer((req, res) => {
    res.writeHead(200, {"content-type": "text/plain"});
    res.end("hola");
  }).listen(puerto);
}

El primer tropiezo vale más que varias de las mediciones.

Lo publicado, y lo que no, medido

Todo en la misma máquina, un Ryzen 7 7800X3D con 30 GB de RAM, en CPU. El servidor fijado a dos núcleos con taskset y el generador de carga a otros seis, para que no compitan por el mismo planificador: si compartieran núcleos, estaría midiendo cuál de los dos pierde la pelea.

anuncio de bun 1.4 · contra mi banco de pruebas
1/ 2se sostienen al medirlas
Arranque en frío, en Linuxse sostiene
dicen 10% más rápido

61,4% más rápidoDe 7,58 ms a 2,93 ms, mediana de las 60 corridas del gráfico de abajo. El 10% publicado es el arranque del CLI de Claude Code corriendo sobre Bun, no un número general de Bun; mi caso es más estrecho, un guion de una línea.

Tamaño del binariono llega
dicen 20% menos

12,9% menosDe 92,75 MB a 80,76 MB, pesando el binario que baja del instalador oficial.

La marca gris de cada barra es lo que publica el anuncio; la barra es lo que salió al medirlo. De memoria y de CPU en reposo Bun no publica ningún número, así que esas dos tienen su propia medición más abajo, sin marca de comparación porque no existe. Y falta la fila de peticiones por segundo: la retiré al descubrir que el servidor y el generador compartían núcleos físicos. Se explica más abajo.

El arranque es real y es enorme

Es la diferencia más grande de todas, y la más fácil de comprobar: ejecutar un archivo con un console.log y cronometrar.

arrancar y ejecutar una línea · 60 corridas cada uno · ms

bun 1.4.0 · Rust2,93ms

La nube más apretada de las tres: 2,85 a 3,10 ms.

bun 1.3.14 · Zig7,58ms

node 22.23.215,81ms

Las 60 corridas de cada candidato, en ms
candidatomedianamínimomáximo
bun 1.4.0 · Rust2,932,853,10
bun 1.3.14 · Zig7,587,458,63
node 22.23.215,8114,7818,07

Las corridas caen en el orden real en que se ejecutaron, y la mediana se recalcula con cada una. Las tres nubes terminan sin tocarse: cuando la separación es así de limpia, la diferencia es del programa y no de una corrida afortunada.

Un 61,4% más rápido sobre este caso, muy por encima del 10% que anuncia Bun para el arranque del CLI de Claude Code sobre la 1.4 en Linux, un caso más específico que el mío: una herramienta real corriendo encima, en vez del binario solo. Con una salvedad: esto es un console.log con el binario ya en la caché del sistema. No ejercita módulos, ni resolución de dependencias, ni transpilación, y esas son justo las etapas que un arranque real sí paga. Y el binario en Rust es además el más estable de los tres: sus sesenta corridas caben en 250 microsegundos, mientras que Zig tiene un pico suelto de 8,63 ms y Node uno de 18,07.

Esa estabilidad es la razón de publicar la mediana y no el promedio. Un solo pico del planificador arrastra un promedio entero, y en una máquina compartida siempre hay uno.

La CPU en reposo no se puede medir con las herramientas obvias

Aquí me equivoqué de instrumento antes de acertar. Un servidor levantado y sin nadie pidiendo nada debería consumir prácticamente cero, y eso es justo lo que hace difícil medirlo.

La primera pasada usó los contadores de /proc/<pid>/stat, que cuentan en tics de diez milisegundos. Sobre trescientos segundos dio ocho tics para Zig y un tic para Rust. Dividir eso arroja «ocho veces menos», que suena a resultado. No lo es: con un tic de resolución no se distingue «ocho veces menos» de «nada medible», y Node en esa misma pasada dio cero tics, o sea infinito.

La herramienta correcta es /proc/<pid>/schedstat, que expone el tiempo en CPU en nanosegundos, seis órdenes de magnitud mejor. Para sumar todos los hilos del proceso hay que recorrer cada uno por separado, en /proc/<pid>/task/<tid>/schedstat:

def ns_cpu(pid):
    """Nanosegundos de CPU del proceso, sumando todos sus hilos."""
    total = 0
    for tid in os.listdir(f"/proc/{pid}/task"):
        total += int(open(f"/proc/{pid}/task/{tid}/schedstat").read().split()[0])
    return total

Con esa resolución el número aparece limpio, y es el más favorable de los cinco.

schedstat · CPU acumulada de un servidor ocioso · 300 s
215,1bun 1.3.14 · Zig
22,2bun 1.4.0 · Rust
150 s
levantado5 min
bun 1.3.14 · Zig · 215,1 msgasta CPU en los 299 segundosbun 1.4.0 · Rust · 22,2 mssolo en 47 de 299

Nadie le pide nada al servidor en estos cinco minutos. La curva de Zig sube en todos y cada uno de los 299 segundos; la de Rust se queda plana y da 47 escalones sueltos. El total importa menos que la forma: uno se despierta sin parar y el otro duerme de verdad.

Nueve coma tres veces menos. Bun no publica ningún número de CPU en reposo; esta cifra es enteramente mía. Y el total es lo menos interesante del gráfico.

Lo que se ve al reproducirlo es que las dos curvas tienen forma distinta. La de Zig es una rampa: sube en los 299 segundos, sin saltarse ninguno. La de Rust es una escalera casi plana, con 47 escalones en toda la ventana. Rust no abarata cada despertar. Se despierta seis veces menos.

Esta es la única cifra que no medí en la misma máquina que el resto. A media tarde la máquina de pruebas volvió a llenarse de trabajo ajeno, y el reposo es precisamente lo más sensible a eso: un proceso ocioso en una máquina saturada se despierta más veces por contención y acumula CPU que no es suya. Los números de aquí salen de un servidor de cuatro núcleos con carga 0,05, donde medir un proceso dormido no molesta a nadie.

Y hay que decir algo sobre la precisión del múltiplo. Repetí el experimento tres veces y dio 8,8, luego 13,8 y luego 9,3, en la misma máquina y con el mismo método. Cuando el numerador y el denominador son ambos diminutos, la razón entre ellos baila. El múltiplo exacto no es un dato firme; que el proceso en Rust duerma mucho más que el de Zig, sí, porque eso se sostuvo en todas las corridas.

Vale una aclaración honesta: los dos consumen una cantidad de CPU irrelevante en reposo. Doscientos dieciséis milisegundos en cinco minutos no le arruinan el día a nadie. Donde esto importa es al multiplicar por cientos de procesos, que es exactamente el caso que el proyecto menciona.

La memoria mejora, pero no tanto como dicen

RuntimeRecién levantadoTras atender tráfico
bun 1.3.14 · Zig36,4 MB44,5 MB
bun 1.4.0 · Rust16,2 MB27,5 MB
node 22.23.248,9 MB62,0 MB

Aquí está el matiz. Recién levantado el proceso, la mejora es del 55,5%. Después de atender tráfico real baja al 38,2%. Bun no publica una cifra de memoria para servidores HTTP: lo más cercano que dice es una reducción de memoria en builds repetidos con Bun.build(), un escenario distinto al que mido aquí. Así que esto es medición propia de punta a punta.

El número depende de cuándo se mire. Si te importa lo que va a ocupar el proceso en producción, la cifra útil es la de después del tráfico, y esa es 38,2%.

Dicho eso, la mejora es grande de todos modos, y ambas versiones de Bun quedan muy por debajo de Node.

Las peticiones por segundo: retiro esta medición

Aquí publiqué un empate, y estaba mal medido. Lo dejo escrito con el error a la vista porque la causa es la clase de trampa que se repite.

Fijé el servidor a los núcleos 10 y 11, y el generador de carga a los núcleos 0 al 5. Por los números parecen conjuntos separados. No lo son:

$ cat /sys/devices/system/cpu/cpu10/topology/core_cpus_list
2,10
$ cat /sys/devices/system/cpu/cpu11/topology/core_cpus_list
3,11

La CPU 10 es el hermano lógico de la 2, y la 11 lo es de la 3. En un procesador con SMT, dos «CPUs» con números lejanos pueden ser el mismo núcleo físico. Durante toda esa medición el servidor y el generador pelearon por dos núcleos físicos, así que lo que medí fue esa pelea.

El número que publiqué era 84.162 contra 84.508. Lo retiro: no puedo afirmar ni empate ni mejora a partir de ahí. La medición nueva, en contenedores con límites y con los núcleos verificados por topología en vez de por número, es la que se ve corriendo en el video de arriba.

Y la lección, que vale más que la cifra: taskset no avisa. Acepta cualquier lista de números y el trabajo parece aislado. La única forma de saberlo es preguntar por la topología.

La distancia con Node sí sobrevive al error, porque es de otro orden de magnitud: casi el doble de peticiones servidas con el mismo código. Dos runtimes peleando por un núcleo compartido no se vuelven el doble de rápidos entre sí por eso.

Instalar dependencias: sin cambios

El uso más frecuente de Bun en el día a día no es servir HTTP, es instalar paquetes. Con ocho dependencias comunes y la caché borrada entre corridas:

RuntimeInstalación en frío
bun 1.3.14 · Zig2,43 s
bun 1.4.0 · Rust2,34 s

Empate otra vez, dentro del margen. El gestor de paquetes está limitado por la red y el disco, así que tiene sentido que un cambio de lenguaje no lo mueva.

Qué aprendí midiendo esto

Que un proyecto publique cifras favorables sobre sí mismo no significa que estén infladas. De las que Bun publica, el arranque la supera con holgura y el binario se queda algo corto; la de peticiones por segundo la retiré por el error de medición. Y hay dos cifras que Bun simplemente no publica, memoria en servidores HTTP y CPU en reposo, que solo existen porque las medí yo.

Lo que sí cambia al medirlo uno mismo es el matiz, y el matiz es lo que sirve para decidir:

Cuándo lo usaría y cuándo no

Actualizaría a la 1.4 sin dudarlo si el arranque importa: scripts, ganchos, tareas programadas, contenedores que arrancan y mueren. Ahí el 61% es real y se siente.

Actualizaría también por la memoria, con expectativas ajustadas: cuenta con un tercio menos, no con la mitad.

No actualizaría esperando más peticiones por segundo, porque en mi medición no las hay. Y no cambiaría de Node a Bun solo por el consumo en reposo: los dos gastan tan poco que la diferencia solo importa multiplicada por cientos de procesos.

Con una advertencia que pone la aritmética por mí: esta es la primera versión estable sobre una base de código nueva, traducida entera en once días. Los números están bien. La cantidad de superficie recién estrenada es enorme, y eso no lo mide un banco de pruebas.

Cómo reproducirlo

Todo lo que hay arriba sale de cuatro guiones cortos: uno para el arranque, uno para memoria y CPU, uno para el reposo en nanosegundos y uno que envuelve a oha. El servidor de prueba son las doce líneas de más arriba.

Las condiciones importan más que los guiones. A media medición la máquina tenía otro trabajo encima y los tiempos de arranque se duplicaron: 13,77 ms para Zig donde con la máquina libre daba 7,58. Las proporciones aguantaron, los absolutos no. Todo lo que se publica aquí está medido con la máquina en reposo, verificado con uptime antes de cada corrida, y con los procesos fijados a núcleos concretos.

Fuentes

BOLETÍN · MEDIDO

Una medición nueva, cuando la hay.

Cada post con sus números, sus fallos y los scripts para repetirlo. Entre uno y tres correos al mes. Te das de baja con un clic.

LO ÚLTIMO QUE LLEGÓ

Uso Listmonk en mi propio servidor; tu correo no sale de ahí. Confirmas por correo antes de recibir nada.

Comentarios

Todavía no hay comentarios. El primero es tuyo.

Se revisa antes de publicarse. El correo no se guarda ni aparece en ninguna parte.