|

Chunk 404 deploy: deploy verde y producción rota

En pocas palabras: Un deploy exitoso rompe producción cuando una pestaña abierta pide un chunk con hash que el deploy nuevo borró y recibe 404. Se arregla recargando una vez ante vite:preloadError, sirviendo el HTML con Cache-Control: no-cache y activando Skew Protection en Vercel (planes Pro y Enterprise).

Un chunk 404 deploy es lo que pasa cuando una pestaña abierta antes de un deploy pide un archivo JavaScript que el deploy nuevo borró: el pipeline dice success y la consola dice 404. Se arregla recargando una vez ante vite:preloadError, sirviendo el HTML con no-cache y activando Skew Protection en Vercel.

«Failed to fetch dynamically imported module» es el mensaje que Chrome registra cuando un import() dinámico pide un chunk de JavaScript y el servidor responde 404. En Vite, cada chunk lleva un hash de contenido en el nombre (por ejemplo settings-<hash>.js). Si un deploy reemplaza los archivos, las pestañas que cargaron la versión anterior apuntan a nombres que ya no existen. Skew Protection es la función de Vercel que fija cliente y servidor a la misma versión.

En 30 segundos

  • El síntoma: una pestaña abierta antes del deploy no responde al clic, la consola muestra 404 en /assets/settings-<hash>.js y una pestaña nueva anda bien.
  • Fix en el cliente: escuchar vite:preloadError y recargar una sola vez, con un guard de 10 segundos en sessionStorage para evitar un loop.
  • Fix en el servidor: HTML con Cache-Control: no-cache y, en Vercel, Skew Protection (planes Pro y Enterprise, un día de vigencia por defecto).
  • No todo es desfase de versiones: un lockfile ausente, una caché de build vieja, una función colgada o un Instant Rollback pueden dejar producción rota con el log en verde.
  • Un smoke test en el CI contra el dominio de producción, con /api/health devolviendo el commit, detecta alias viejos y pushes que dejaron de desplegar.

¿Qué significa «Failed to fetch dynamically imported module» después de un deploy?

Significa que el navegador intentó cargar un chunk por su nombre y el servidor ya no lo tiene. Vite le pone un hash de contenido a cada chunk, y el import() dinámico recién se ejecuta cuando el usuario hace clic. Si entre la carga de la página y ese clic hubo un deploy, el archivo viejo desapareció y la petición devuelve 404.

Ponele que un cliente deja abierto tu panel toda la mañana, desplegás a las 11 y él toca “Configuración”. No pasa nada. Abre una pestaña nueva y todo funciona. Ese desfase es lo que hace tan difícil reproducirlo desde un reporte de bug: quien lo sufre tiene una pestaña vieja y vos probás con una nueva. Sobre eso hablamos en publicar una app React con Vite.

El caso sale de un análisis publicado en dev.to el 12 de octubre de 2026, donde el autor leyó 4.704 posts públicos de gente cuyas apps fallaron al lanzar y verificó 538 casos recientes. Deploy y build es la categoría más grande, con 23%, y este patrón suma 22 reportes verificados. Ojo: son cifras del propio autor, no un dato oficial de Vercel, y 22 reportes no alcanzan para estimar qué tan común es en tu proyecto.

¿Cómo reproducir el error de chunk 404 en cinco minutos con Vite?

Con un proyecto Vite de un solo módulo lazy, una carpeta que hace de host y un servidor estático, reproducís el chunk 404 deploy en cinco minutos: desplegás v1, dejás la pestaña abierta, desplegás v2 borrando los assets viejos y hacés clic en esa pestaña. Estos son los pasos del autor:

  • Proyecto mínimo. Un index.html con un botón, un main.js que hace import(‘./settings.js’) al clic y “type”: “module” en package.json.
  • Host simulado. Una carpeta live/ servida con python3 -m http.server 4173 -d live.
  • Deploy v1. Buildeás, copiás dist/ a live/, abrís localhost:4173 y no tocás esa pestaña.
  • Deploy v2. Cambiás settings.js, volvés a buildear, borrás live/assets con rm -rf y copiás el build nuevo.
  • Clic en la pestaña vieja. No hace nada, el panel de red muestra el 404 y Chrome registra el error.

El autor también publicó un test de Playwright que carga la página, hace un segundo deploy por debajo y después hace clic. Falla a los 10 segundos si cada clic reintenta un chunk inexistente, y pasa si la página se recarga una vez o si todavía recibe el chunk viejo. Un botón muerto no es aceptable.

Mi lectura (inferencia, no algo que figure en la fuente): como pasa en cualquiera de los dos casos, el test no te obliga a elegir la solución. Mide el resultado que ve el usuario.

¿Cómo arreglar los chunk 404: vite:preloadError, no-cache y Skew Protection en Vercel?

Hay tres capas y conviene combinarlas. En el cliente, recargá una sola vez al recibir vite:preloadError. En el servidor, servís el HTML con Cache-Control: no-cache. En Vercel, activás Skew Protection para que la pestaña vieja siga hablando con el deployment que la sirvió. Tema relacionado: guía para desplegar en Vercel gratis.

Para el cliente, la documentación de Vite explica que el evento trae el error original en event.payload y que, si llamás a event.preventDefault(), el error no se lanza. El guard del ejemplo evita que un chunk realmente faltante te deje recargando en loop:

window.addEventListener('vite:preloadError', (event) => {
 const last = Number(sessionStorage.getItem('chunk-reload') || 0)
 if (Date.now() - last < 10000) return
 event.preventDefault()
 sessionStorage.setItem('chunk-reload', String(Date.now()))
 window.location.reload()
})

El header no-cache en el HTML importa porque, según Vite, sin él el navegador sigue referenciando los assets viejos. Si servís desde tu propio servidor (hosting o VPS, por ejemplo en donweb.com), esa configuración es tuya. Skew Protection no existe ahí.

Del lado de Vercel, esto dice la documentación oficial:

  • Qué fija. Assets estáticos, navegaciones del lado del cliente con Server Actions y prefetches, agregando ?dpl= o el header x-deployment-id.
  • Dónde está disponible. Planes Pro y Enterprise, con Next.js, SvelteKit, Qwik, Astro y Nuxt. Un Vite sin framework no figura en la lista, aunque otros frameworks pueden implementarlo chequeando VERCEL_SKEW_PROTECTION_ENABLED y mandando VERCEL_DEPLOYMENT_ID.
  • Qué no cubre. Los fetch() personalizados y las navegaciones de página completa: un hard refresh recibe el último deployment y el cliente recarga si detecta el desfase.
  • Cuánto dura. Un día por defecto, ampliable hasta el límite de Deployment Retention. Pasada esa edad, el pedido devuelve 404.
  • Cómo se activa. Los proyectos creados después del 19 de noviembre de 2024 con framework soportado ya lo traen. En los anteriores va en Settings, Advanced, Skew Protection, y después un redeploy.
  • Deploys prebuilt. Con vercel deploy –prebuilt hace falta un deployment ID propio, y hoy solo lo soportan Next.js y la Build Output API.

Mi criterio, y es opinión: dejaría el guard de recarga aunque tengas Skew Protection, porque la protección vence cuando se cumple el max age y un dashboard abierto una semana lo supera.

¿Qué otras causas hacen que un deploy exitoso deje producción rota?

Además del desfase de versiones, el análisis de dev.to describe cuatro formas de “verde pero roto”: un build de producción distinto al local, una caché de build vieja, funciones que esperan algo inalcanzable y la automatización de deploy apagada. En todos los casos el log del host termina en success, así que nadie lo mira.

SíntomaCausa probableQué revisar
Botón muerto en pestaña vieja, 404 en /assets/*.jsDesfase de versiones: el deploy borró los chunks viejosvite:preloadError, no-cache en el HTML, Skew Protection
Falta un stylesheet o la app se comporta distinto que en localEl host hace su propia instalación: otra versión de Node, sin lockfile, otro directorio de salidanpm ci, .nvmrc o .node-version, directorio de salida configurado
El build de Vercel difiere de un build local limpioCaché de build vieja (la clave incluye proyecto, preset, root directory, Node, package manager y rama)Rebuild sin caché con vercel –force
504 en una ruta de API que en local responde al instanteVariable de entorno faltante, base que rechaza la red del host o código que nunca respondeVariables, reglas de red de la base, respuesta en todos los caminos
Los pushes a producción no generan deploy o el alias sirve un build viejoAuto-asignación apagada tras un Instant Rollback, webhook faltante, Ignored Build Step que sale con 0 o git.deploymentEnabled en falseUndo Rollback, vercel promote, configuración de Git
chunk 404 deploy diagrama explicativo

El caso del rollback es el más traicionero. Imaginate el cuadro: hacés rollback a las apuradas porque algo se rompió, el incidente se apaga, volvés a pushear el fix a la rama de producción, esperás un deploy que nunca llega, revisás el webhook, revisás el Ignored Build Step, y recién después alguien nota que el tile de producción tiene un botón que dice Undo Rollback.

Según la documentación de Vercel (actualizada el 7 de julio de 2026), después de un Instant Rollback la plataforma apaga la auto-asignación de dominios de producción, así que los pushes nuevos no reemplazan el deployment revertido. Para volver a la normalidad, usás Undo Rollback en el tile o ejecutás vercel promote con la URL de un deployment. Tené en cuenta que el rollback no actualiza variables de entorno cambiadas después y que los cron jobs vuelven al estado del deployment revertido.

Con el lockfile pasa algo parecido, en versión silenciosa: sin lockfile commiteado, ^4.0.0 puede resolver a una versión más nueva que la de tu máquina. Lo explicamos a fondo en un deploy verde que igual mentía.

¿Cómo verificar producción después de cada deploy con un smoke test?

Corré un script en tu CI después de cada push, contra el dominio de producción y no solo contra la URL del deployment. Pide la home, pide el primer JavaScript de /assets/ y llama a /api/health con un límite de 10 segundos, verificando que responda el commit que acabás de pushear. Un alias revertido o un hook de Git desconectado solo aparecen en el dominio.

Tiene que correr en el CI y no en el build del host, porque un host que dejó de desplegar también deja de ejecutar tus chequeos. Para que funcione, /api/health debe devolver el commit del build, por ejemplo escribiendo git rev-parse HEAD durante el build.

set -e
url=https://tu-dominio.example
curl -fsS "$url/" -o /tmp/index.html
asset=$(grep -oE '/assets/[^"]+\.js' /tmp/index.html | head -1)
curl -fsS "$url$asset" -o /dev/null
curl -fsS --max-time 10 "$url/api/health" | grep -q "$(git rev-parse HEAD)"

Según el autor, curl -f falla con cualquier estado de 400 o más, así que un stylesheet o chunk faltante rompe el script. –max-time 10 convierte una función colgada en un fallo en vez de una espera larga, y la última línea falla si lo que está en vivo no es lo que pusheaste. Ajustá el patrón del asset a la ruta de salida de tu framework.

Una propuesta editorial, no del autor ni probada por nosotros: si tu app tiene rutas lazy, pedí también un chunk de esas rutas, porque head -1 probablemente agarre el script de entrada y no uno cargado por import() dinámico. Esto se conecta con lo que analizamos en errores típicos de deploys full stack.

La checklist del artículo, para cerrar:

  • Instalación reproducible. npm ci con package-lock.json y .nvmrc commiteados (Netlify también lee .nvmrc o .node-version).
  • Backend primero. Desplegá la API antes que el cliente y dejá los campos y endpoints viejos funcionando hasta que ningún cliente los use.
  • Respuesta en todos los caminos. Cada función devuelve una respuesta, errores incluidos, para que una falla sea un código de estado y no un timeout.

Qué está confirmado y qué no

Confirmado por la documentación de Vercel y Vite: el comportamiento de vite:preloadError, la necesidad de no-cache en el HTML, el alcance y los límites de Skew Protection, y el apagado de la auto-asignación tras un Instant Rollback. Confirmado por el autor de dev.to, con su propio método: el repro local y el comportamiento de los dos tests.

No está confirmado: qué tan frecuente es este patrón fuera de la muestra del autor, cómo se comporta Skew Protection con un Vite sin framework en Vercel (la documentación no lo lista) y si el guard de 10 segundos te sirve tal cual en tu framework. Eso lo tenés que probar vos.

Errores comunes

  • Recargar sin guard. Si el chunk falta de verdad, la página se recarga en loop. El guard en sessionStorage corta eso.
  • Chequear solo la URL del deployment. Un alias viejo o un rollback no se ven ahí, solo en el dominio de producción.
  • Correr el smoke test dentro del build del host. Si el host dejó de desplegar, tu test tampoco corre y nadie se entera.
  • Asumir que Skew Protection cubre todo. No fija los fetch() personalizados ni las navegaciones de página completa por defecto.

Preguntas Frecuentes

¿Por qué me da 404 en un chunk de JavaScript después de hacer un deploy?

Porque la pestaña cargó el HTML antes del deploy y sigue pidiendo chunks con el hash viejo, que el deploy nuevo ya borró. Una pestaña nueva anda bien porque lee los nombres actuales.

¿Qué es Skew Protection en Vercel?

Es una función de Vercel que bloquea la versión para que cliente y servidor usen el mismo deployment. Está disponible en planes Pro y Enterprise con Next.js, SvelteKit, Qwik, Astro y Nuxt, y el framework agrega el ID del deployment a los pedidos de assets, navegaciones y prefetches.

¿Cómo se activa Skew Protection?

Entrá a Settings, Advanced y activá el switch de Skew Protection, con la variable de sistema habilitada, y hacé un redeploy de producción. Los proyectos creados después del 19 de noviembre de 2024 con un framework soportado ya lo traen activado.

¿Por qué mis pushes a producción ya no generan deploy después de un rollback?

Porque Vercel apaga la auto-asignación de dominios de producción tras un Instant Rollback. Para revertirlo, usá Undo Rollback en el tile de producción o corré vercel promote con un deployment.

¿Cómo hago un smoke test después de cada deploy?

Corré en el CI un script con curl -f que pida la home, el primer JS de /assets/ y /api/health con –max-time 10, comparando contra git rev-parse HEAD. Ejecutalo contra el dominio de producción, no contra el build del host.

Conclusión

Un deploy en verde no garantiza que producción funcione, y el chunk 404 es el ejemplo más fácil de reproducir. Lo mínimo que haría es agregar el guard de vite:preloadError, poner no-cache en el HTML y, si estás en Vercel con un framework soportado, revisar que Skew Protection esté activo y con un max age acorde a tus sesiones. Después sumaría el smoke test al CI, con /api/health devolviendo el commit. Con eso cubrís el desfase de pestañas y también los alias viejos y los pushes que dejaron de desplegar.

Fuentes

Te puede interesar...