
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.
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.
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.
- Arranque en frío, en Linuxse sostiene
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
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.
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
| candidato | mediana | mínimo | máximo |
|---|---|---|---|
| bun 1.4.0 · Rust | 2,93 | 2,85 | 3,10 |
| bun 1.3.14 · Zig | 7,58 | 7,45 | 8,63 |
| node 22.23.2 | 15,81 | 14,78 | 18,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.
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
| Runtime | Recién levantado | Tras atender tráfico |
|---|---|---|
| bun 1.3.14 · Zig | 36,4 MB | 44,5 MB |
| bun 1.4.0 · Rust | 16,2 MB | 27,5 MB |
| node 22.23.2 | 48,9 MB | 62,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:
| Runtime | Instalación en frío |
|---|---|
| bun 1.3.14 · Zig | 2,43 s |
| bun 1.4.0 · Rust | 2,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:
- El arranque mejoró mucho más de lo que Bun publica, y es la diferencia que de verdad se nota en herramientas de línea de comandos, en ganchos de git y en funciones que arrancan por petición.
- La memoria mejora bastante, aunque no hay cifra oficial contra la cual medirla; depende de si miras después del tráfico o al levantar el proceso.
- El rendimiento HTTP no se movió, al menos en el caso más simple posible.
- Y en reposo la diferencia no está en gastar menos por despertar, sino en despertar mucho menos: 47 segundos con actividad contra 299.
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
- Rewriting Bun in Rust — el anuncio del proyecto, de donde salen las cifras de arranque, tamaño de binario y peticiones por segundo contrastadas.
- oha — el generador de carga, versión 1.16.0.
proc(5), sección/proc/pid/schedstat— el contador en nanosegundos que hace medible el reposo.
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Ó
- 7 arneses con MiniMax-M329 de 39 con el arnés más simple
- runc vs gVisor vs KataKata arranca 27× más lento
- ComfyUI vs Forge en 16 GB2× con Z-Image
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.