Skip to content
a@o:~$

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

· 4 min read · forensics · node.js · local-dev · debugging

The zombie on port 3000: a restart that never happened

A green build, a freshly 'restarted' server, and every route answering 404. Nobody had broken anything — we were just interrogating a corpse. A short forensic on orphaned processes, and why 'I restarted it' is a claim that needs evidence.

symptom

Local production server, minutes after a clean build: every route — pages that existed, pages that didn't, even image routes — answered 404. The server log repeated one line, unhelpfully:

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

The same commit was serving production flawlessly at that exact moment. So the code was innocent, the build was innocent, and yet localhost was a wall of 404s. When the same artifact behaves differently in two places, stop reading the artifact and start reading the processes.

evidence

  • The server had been "restarted" moments earlier — its supervisor had even reported the old one as dead.
  • Between the old start and the failure, the .next build directory had been deleted and rebuilt twice — while something was still holding it.
  • The smoking gun arrived when the "new" server's log was finally read end to end:
the 'restarted' server's log
Error: listen EADDRINUSE: address already in use :::3000
  code: 'EADDRINUSE',
  syscall: 'listen',
  port: 3000

root cause

The restart never happened. The old Node process had outlived its supervisor — the task wrapper died, the process didn't — and kept squatting on port 3000. Every "new" server since then was born, found the port taken, printed EADDRINUSE into a log nobody read, and exited. Meanwhile the squatter had had its .next directory rebuilt underneath it twice: its in-memory route manifests pointed at files that no longer existed, which Next surfaces as NoFallbackError and a 404 for absolutely everything.

A zombie: dead to its supervisor, alive to the operating system, and answering requests with the routing table of a build that had been deleted half an hour earlier.

the crime scene, reenacted

Words only carry so far. Below is the incident itself, undead and interactive: try to restart the server and watch the lie happen in real time. The zombie only goes down one way.

localhost:3000 — 404 Not Found
// you arrive at the scene. every request is 404ing. try something:
exhibit ZInteractive reenactment. The zombie only dies if you kill the PID; restarting just feeds it.

intervention

Ask the OS — the only witness that doesn't lie about processes — who actually owns the port, kill that PID, and start clean:

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

lessons

  • "Restarted" is a claim, not a fact.The proof is the port owner's PID changing — not the supervisor's report, not the absence of an error you didn't scroll down to see.
  • Never rebuild artifacts underneath a live process. A server reading .next while you replace it enters a state nobody tested: not the old build, not the new one, but a chimera of both.
  • Supervisors and task managers track their children, not the truth. Processes get orphaned; the OS port table is the source of record.
  • This is the quiet cousin of the bug that worked: the zombie didn't crash, didn't scream, didn't die. It just kept politely answering 404 — and the error that explained everything was sitting unread in the log of a process that lived for two seconds.

The most dangerous line in a log is the one printed by a process you believe is running — but was written by one that already failed to start.