Skip to content

Instantly share code, notes, and snippets.

@td0m
Created April 25, 2025 15:10
Show Gist options
  • Save td0m/409a3b228dfe24f3135c7289c0b0382c to your computer and use it in GitHub Desktop.
Save td0m/409a3b228dfe24f3135c7289c0b0382c to your computer and use it in GitHub Desktop.
NodeJS: Zero Downtime with `node:cluster`
import cluster from "node:cluster";
import { availableParallelism } from "node:os";
export async function clustered({
numWorkers = availableParallelism(),
primary,
worker,
recoverWorkers = true,
shutdownPrimary,
}: {
numWorkers?: number;
primary?: () => Promise<void>;
worker: () => Promise<void>;
recoverWorkers?: boolean;
shutdownPrimary?: () => Promise<void>;
}) {
if (cluster.isPrimary) {
process.on("SIGHUP", () => {
console.info("SIGHUP");
const workersAtSighup = Object.values(cluster.workers || {}).filter(
(worker) => worker?.isConnected()
);
for (const worker of workersAtSighup) {
const newWorker = cluster.fork();
newWorker.on("listening", () => {
console.info(`killing worker ${worker?.process.pid}`);
worker?.disconnect();
});
}
});
const shutdown = async () => {
for (const worker of Object.values(cluster.workers || {})) {
worker?.disconnect();
}
if (shutdownPrimary) {
await shutdownPrimary();
}
console.log("exiting now");
process.exit(0);
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on("SIGTERM", async () => {
console.info("SIGTERM");
await shutdown();
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
process.on("SIGINT", async () => {
console.info("SIGINT");
await shutdown();
});
// Fork
for (let i = 0; i < numWorkers; i++) {
cluster.fork();
}
if (recoverWorkers) {
cluster.on("exit", (worker, code, signal) => {
// we did this on purpose, so we don't need to restart the worker
if (worker.exitedAfterDisconnect) {
console.info(
`worker ${worker.process.pid} exited after disconnect. not restarting.`
);
return;
}
console.info(
{ worker_pid: worker.process.pid, signal, code },
"worker exited, restarting..."
);
cluster.fork();
});
}
if (primary) {
await primary();
}
} else {
await worker();
}
}
import { createServer } from "node:http";
import { clustered } from "./clustered.ts";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Hello, World!\n");
});
await clustered({
numWorkers: 3,
primary: async () => {
console.log("primary started!", process.pid);
},
worker: async () => {
console.log("worker started!", process.pid);
server.listen(3000);
},
});
@td0m
Copy link
Author

td0m commented Apr 25, 2025

To run it (requires node 23 or above for type stripping):

NODE_NO_WARNINGS=1 node index.ts

To send the HUP signal (just like systemctl restart would), find out the PID of the main process and run:

kill -HUP {{PID}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment