Probé Bun 1.0 en octubre de 2023. Duré aproximadamente tres semanas antes de volver a Node.js con la cola entre las patas. En ese momento la compatibilidad con el ecosistema npm era, digamos, aspiracional. Muchos paquetes simplemente no funcionaban — el error más frecuente era un TypeError: X is not a function en cosas que corrían perfectamente en Node. Archivé el experimento y seguí con mi vida.
Entonces llegó enero de 2026 y decidí darle otra oportunidad. Bun 1.2 tenía un nivel de compatibilidad que, honestamente, no esperaba. Llevamos seis semanas en producción. Esto es lo que pasó.
Por Qué Siquiera Vale la Pena Revisitarlo en 2026
Somos un equipo de cuatro personas construyendo una herramienta de análisis para equipos de desarrollo — una API REST que procesa webhooks de GitHub y GitLab, los almacena, y sirve dashboards de métricas. Unos 18 endpoints, PostgreSQL con pg, Redis para caché con ioredis, corriendo en AWS ECS con containers Docker sobre instancias Graviton2 (arm64). Nada exótico.
La motivación original era el tiempo de arranque de los containers. Teníamos cold starts notables en ciertos períodos de bajo tráfico cuando ECS escalaba desde cero, y alguien del equipo mencionó que Bun debería ayudar ahí. Mi expectativa era validarlo rápido, decir "sí, arranque más rápido, pero no vale la migración" y cerrar el ticket.
No fue así.
Lo que cambió mi evaluación no fue el throughput de la API en producción — fue algo que no estaba midiendo: el tiempo de nuestros tests. Pero eso viene después.
Los Benchmarks Reales: Qué Números Importan y Cuáles No
Lo primero que medí fue startup. Simple: time node index.js vs time bun index.js en nuestra aplicación real, no un hello world. Con Node.js 22.14 tardábamos unos 820ms en llegar a "listening on port 3000". Con Bun 1.2.4: 180ms.
Eso es real. Y en nuestro caso específico, esa diferencia importa.
Para throughput, armé un benchmark con wrk:
-- wrk_test.lua
-- Simula tráfico real: webhook POST con body JSON típico
wrk.method = "POST"
wrk.body = '{"ref":"refs/heads/main","repository":{"id":12345,"name":"api"}}'
wrk.headers["Content-Type"] = "application/json"
wrk.headers["X-Hub-Signature-256"] = "sha256=abc123"
# 30 segundos, 50 conexiones concurrentes, 4 threads
wrk -t4 -c50 -d30s -s wrk_test.lua http://localhost:3000/webhooks/github
En mi MacBook Pro M3 con 36GB RAM:
- Node.js 22: ~8,400 req/s, latencia media 5.9ms
- Bun 1.2: ~12,100 req/s, latencia media 4.1ms
Un 44% más de throughput. Suena impresionante. El problema es que en producción real en ECS, con las mismas instancias t4g.medium, la diferencia se comprimió bastante — quedamos en torno a un 18-22% de mejora en throughput. Sigue siendo una mejora real y medible, pero los benchmarks locales claramente sobreestiman la ganancia. Cuánto exactamente depende de tu carga de I/O, tu pool de conexiones, y mil factores más.
La memoria me sorprendió en la dirección contraria. Esperaba que Bun fuera más liviano — toda la narrativa de marketing habla de eficiencia. En nuestra app, en estado estable con tráfico normal, Bun usaba aproximadamente un 12% más de RAM que Node. No es catastrófico para nosotros, pero no era lo que anticipaba. No tengo una explicación definitiva — podría ser cómo Bun gestiona el heap internamente, podría ser nuestro código específico. No voy a afirmar que este número se generaliza a cualquier proyecto.
El cold start sí mejoró en ECS. El tiempo promedio desde que el task se marca healthy hasta que sirve el primer request bajó de ~4.2 segundos a ~1.8 segundos. Eso sí cambió algo concreto: pudimos reducir el minimumHealthyPercent en nuestros deployments sin ponernos nerviosos.
Lo Que Nadie Te Cuenta Sobre Compatibilidad en arm64
Aquí se pone interesante. Y frustrante.
pg, ioredis, Express — todo funcionó desde el día uno. Sin tocar nada. dotenv, pino para logging, zod para validación: sin problemas. Hasta ahí, la compatibilidad de Bun 1.2 con el ecosistema npm estándar es genuinamente sólida.
El primer problema gordo vino de un módulo interno nuestro que usa node:crypto. Específicamente, crypto.createCipheriv con AES-256-GCM y AAD (Additional Authenticated Data). En Bun 1.2.4, esto funcionaba en la mayoría de los casos — pero había un edge case con payloads que contenían caracteres no-ASCII en los metadatos donde el resultado difería de Node. Tardé un día entero en identificar este bug. Un día que básicamente fue yo mirando diffs de buffers hexadecimales preguntándome si me estaba volviendo loco.
El fix fue migrar esa lógica a la Web Crypto API, que ambos runtimes implementan de manera consistente:
// La versión "portable" que funciona igual en Node y Bun:
async function encryptWithAAD(keyBuffer, iv, data, additionalData) {
const key = await crypto.subtle.importKey(
'raw',
keyBuffer,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
// crypto.subtle es parte del estándar Web Crypto — comportamiento idéntico en ambos
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, additionalData },
key,
data
);
return new Uint8Array(encrypted);
}
Debería haber usado Web Crypto desde el principio. Lección aprendida.
El segundo problema: sharp para procesamiento de imágenes (tenemos un endpoint de avatars). Sharp usa bindings nativos de C++. En Linux x86_64 funcionó sin drama. En arm64 — nuestro entorno de producción en Graviton2 — necesitamos compilar desde fuente durante el build del container. No imposible, pero sí una hora de debugging de Dockerfile que no estaba en los planes. Si dependes de paquetes con native addons en arm64, verifica antes de asumir que va a funcionar.
Cuatro Semanas de Migración: Lo Que Rompí y Lo Que Mejoré
La migración la hice en fases. Desarrollo primero, staging después, producción al final.
El entorno de desarrollo fue sorprendentemente rápido — dos horas para cambiar los scripts de package.json, actualizar el Dockerfile, y verificar que todo arrancaba. Demasiado fácil. Esa facilidad me dio mala espina.
Staging reveló los problemas de crypto que ya mencioné. También reveló algo que no esperaba para nada: la migración a bun test.
Teníamos nuestros ~180 tests de integración en Jest. Migrar a bun test tomó aproximadamente cuatro horas — la mayoría gastada en diferencias en el sistema de mocks. jest.spyOn tiene equivalente, pero la sintaxis de mockRestore y el comportamiento de mocks automáticos entre módulos es ligeramente distinta. Hay que reescribir algunos tests, no muchos, pero algunos.
Una vez migrado: los tests corrían en 11 segundos en lugar de 34 segundos. Once segundos para 180 tests de integración, incluyendo conexiones reales a una base de datos de test. Ese número cambió la dinámica del equipo más que cualquier mejora de throughput en producción. El ciclo de feedback más corto en desarrollo es el beneficio más tangible del día a día — y no es lo que estaba midiendo cuando empecé este experimento.
Esto es lo que genuinamente no anticipé.
Bueno, después de staging llegó el momento del deployment de producción. Lo empujé un viernes por la tarde. Sí. Ya sé. Técnicamente staging había corrido limpio durante cinco días completos, así que me convencí de que era razonable. A las 7pm, después de enrutar el 10% del tráfico a los nuevos containers, empezamos a ver errores 502 esporádicos en los logs de ECS.
El culpable tardó 40 minutos en aparecer: el healthCheckGracePeriodSeconds de nuestra tarea en ECS era de 15 segundos. Bun arranca rápido, sí, pero la inicialización de nuestro pool de conexiones de PostgreSQL tenía un orden de operaciones ligeramente diferente al de Node, y en cold starts el primer health check de ECS llegaba antes de que el pool estuviera completamente listo para aceptar queries. El container respondía al health check con un 200, pero el primer request real que llegaba durante la inicialización del pool fallaba.
Fix: healthCheckGracePeriodSeconds de 15 a 30 segundos. Diez minutos de cambio, dos horas de susto. A las 9:30pm estábamos al 100% del tráfico. No dormí especialmente bien esa noche.
Mi Veredicto: Cuándo Migrar y Cuándo No Molestarse
Directo al grano.
Migra a Bun si tienes una API REST donde el throughput y la latencia importan, tus dependencias son mayoritariamente paquetes JavaScript puros (o has verificado que los native addons funcionan en tu arquitectura específica), y especialmente si tu equipo corre muchos tests — el test runner solo ya puede justificar el esfuerzo. Los cold starts también mejoran significativamente, lo que importa en entornos con escala dinámica.
Quédate en Node si dependes de native addons que no has verificado en Bun, si tu stack incluye herramientas de build muy específicas que no has probado, o si simplemente no tienes el tiempo para investigar esos bugs de compatibilidad del 5% que inevitablemente aparecen. También quédate en Node si tu equipo no tiene capacidad de absorber el costo de una migración de tests — no es enorme, pero no es cero.
Mi recomendación concreta, la más práctica que puedo dar: empieza por migrar solo los tests a bun test. Sin cambiar el runtime de producción, sin tocar nada más. Si la migración de tests fluye sin demasiados problemas y el tiempo de ejecución mejora, tienes una señal clara de que tu codebase es compatible. Si la migración de tests es una pesadilla, el runtime de producción probablemente también lo será.
Bun en 2026 está suficientemente maduro para producción en la mayoría de los proyectos web. No en todos. La diferencia entre "la mayoría" y "todos" sigue siendo los native addons en arquitecturas no-x86_64 y algunos edge cases en APIs de Node.js poco usadas. Verifica tu caso específico — no asumas que lo que funcionó para mí funcionará para ti.
Nosotros no hemos vuelto a Node. Pero si hubiera tenido tres o cuatro dependencias pesadas de native addons en arm64, esta historia probablemente terminaría diferente.
Top comments (0)