Saltar al contenido
a@o:~$

cat /blog/zombie-on-port-3000.md

· 4 min de lectura · forense · node.js · local-dev · debugging

El zombi del puerto 3000: un reinicio que nunca ocurrió

Un build verde, un server recién 'reiniciado' y todas las rutas respondiendo 404. Nadie había roto nada — estábamos interrogando a un cadáver. Un forense corto sobre procesos huérfanos, y por qué 'ya lo reinicié' es una afirmación que necesita evidencia.

síntoma

Server de producción local, minutos después de un build limpio: todas las rutas — páginas que existían, páginas que no, hasta las rutas de imágenes — respondían 404. El log del server repetía una sola línea, sin ayudar en nada:

next start (log)
✓ Ready in 154ms
Error: Internal: NoFallbackError
Error: Internal: NoFallbackError
Error: Internal: NoFallbackError

El mismo commit estaba sirviendo producción sin un solo error en ese preciso momento. Así que el código era inocente, el build era inocente, y aún así localhost era una pared de 404s. Cuando el mismo artefacto se comporta distinto en dos lugares, deja de leer el artefacto y empieza a leer los procesos.

evidencia

  • El server se había "reiniciado" momentos antes — su supervisor incluso había reportado muerto al anterior.
  • Entre el arranque viejo y la falla, el directorio .next se había borrado y reconstruido dos veces — mientras algo seguía agarrándolo.
  • La pistola humeante llegó cuando por fin se leyó completo el log del server "nuevo":
el log del server 'reiniciado'
Error: listen EADDRINUSE: address already in use :::3000
  code: 'EADDRINUSE',
  syscall: 'listen',
  port: 3000

causa raíz

El reinicio nunca ocurrió. El proceso viejo de Node sobrevivió a su supervisor — el wrapper de la tarea murió, el proceso no — y se quedó ocupando el puerto 3000. Cada server "nuevo" desde entonces nacía, encontraba el puerto tomado, imprimía EADDRINUSE en un log que nadie leyó, y moría. Mientras tanto, al ocupante le habían reconstruido el .next por debajo dos veces: sus manifests de rutas en memoria apuntaban a archivos que ya no existían, lo que Next reporta como NoFallbackError y un 404 para absolutamente todo.

Un zombi: muerto para su supervisor, vivo para el sistema operativo, y contestando peticiones con la tabla de rutas de un build que se había borrado media hora antes.

la escena del crimen, recreada

Las palabras llegan hasta donde llegan. Abajo está el incidente mismo, no-muerto e interactivo: intenta reiniciar el server y mira la mentira ocurrir en tiempo real. El zombi solo cae de una manera.

localhost:3000 — 404 Not Found
// llegas a la escena. todas las peticiones dan 404. intenta algo:
exhibit ZRecreación interactiva. El zombi solo muere si matas el PID; reiniciar nomás lo alimenta.

intervención

Pregúntale al OS — el único testigo que no miente sobre procesos — quién es realmente el dueño del puerto, mata ese PID, y arranca limpio:

powershell
Get-NetTCPConnection -LocalPort 3000 -State Listen
  | Select -Expand OwningProcess | Stop-Process -Force
npm run start   # enlaza, sirve 200s

lecciones

  • «Reiniciado» es una afirmación, no un hecho. La prueba es que el PID dueño del puerto haya cambiado — no el reporte del supervisor, ni la ausencia de un error que no bajaste a leer.
  • Nunca reconstruyas artefactos debajo de un proceso vivo. Un server leyendo .next mientras lo reemplazas entra a un estado que nadie probó: ni el build viejo, ni el nuevo, sino una quimera de ambos.
  • Los supervisores y task managers rastrean a sus hijos, no la verdad. Los procesos se quedan huérfanos; la tabla de puertos del OS es el registro oficial.
  • Este es el primo callado de el bug que funcionaba: el zombi no tronó, no gritó, no se murió. Solo siguió contestando 404 educadamente — y el error que lo explicaba todo estaba sin leer en el log de un proceso que vivió dos segundos.

La línea más peligrosa de un log es la que crees que imprimió el proceso que está corriendo — pero la escribió uno que ya había fallado al arrancar.