Today I learned
How do I restart a Node.js container when its event loop hangs?
A Node server stuck in an endless loop answers nothing, and Docker won't restart it: a failing healthcheck only marks it unhealthy, and restart: acts only when the process exits. Nothing on the stuck thread can notice, so the watchdog lives on a worker thread and kills the process from there.
// main thread
const w = new Worker(new URL('./watchdog.js', import.meta.url), { workerData: 30_000 })
w.unref()
setInterval(() => w.postMessage(0), 1000).unref()
// watchdog.js (worker)
let last = Date.now()
parentPort.on('message', () => { last = Date.now() })
setInterval(() => {
if (Date.now() - last > workerData) process.kill(process.pid, 'SIGKILL')
}, 1000)
The gotcha: in a container your server is PID 1, and the kernel won't deliver that SIGKILL to PID 1. Add init: true to the service in Compose so the server runs as a normal child.
I added this to a game's WebSocket gateway, where every player socket hangs on one thread. Keep it off in development: a breakpoint looks exactly like a hang. Docs: Compose init.