Multi-core launcher
When you build a Node.js application, it only uses a single thread by default. If you have a multi-core server available, it can be useful to run multiple instances of your application to make use of the additional cores.
This MultiCoreLauncher class allows you to run as many instances as you have cores available (or how many instances you configure). NodeJS automatically load-balances incoming HTTP requests among them. It can be used as a light-weight alternative to process managers like pm2.
When one of the instances stops unexpectedly, all instances are stopped. By default, the MultiCoreLauncher will then terminate the whole application. You can override this with the onCrash configuration option.
Usage
import { MultiCoreLauncher } from './multi-core-launcher.js';
const launcher = new MultiCoreLauncher('./server.js');
launcher.start();
If you're using Next.js (with standalone output enabled):
{
"scripts": {
"build": "next build && mv .next/standalone dist",
"postbuild": "esbuild app.ts --bundle --platform=node --outfile=dist/app.js",
"start": "node dist/app.js"
},
"dependencies": {
"esbuild": "*"
}
}
Code
import cluster from 'node:cluster';
import { availableParallelism } from 'node:os';
interface Configuration {
threads: number;
silent: boolean | undefined;
stopGracetimeMs: number;
env: Record<string, string | number>;
onCrash: () => void;
}
const workers = () =>
Object.values(cluster.workers!).filter((w) => !w!.isDead());
const defaultConfiguration: Configuration = {
silent: false,
threads: availableParallelism(),
stopGracetimeMs: 10_000,
env: {},
onCrash: () => process.exit(1),
};
export class MultiCoreLauncher {
private configuration: Configuration;
constructor(worker: string, configuration?: Partial<Configuration>) {
this.configuration = { ...defaultConfiguration, ...configuration };
cluster.setupPrimary({
windowsHide: true,
silent: this.configuration.silent,
execArgv: [worker],
});
}
start: () => void = () => {
['SIGINT', 'SIGQUIT', 'SIGTERM'].forEach((signal) => {
process.on(signal, this.stop);
process.off(signal, this.forceStop);
});
cluster.on('exit', this.crashHandler);
for (let i = 0; i < this.configuration.threads; i++) {
cluster.fork(this.configuration.env);
}
};
private crashHandler = () =>
this.stop().then(() => this.configuration.onCrash());
private stoppingPromise: Promise<void> | undefined = undefined;
stop: () => Promise<void> = () => {
if (this.stoppingPromise) {
return this.stoppingPromise;
}
cluster.off('exit', this.crashHandler);
['SIGINT', 'SIGQUIT', 'SIGTERM'].forEach((signal) => {
process.off(signal, this.stop);
process.on(signal, this.forceStop);
});
if (workers().length <= 0) {
return Promise.resolve();
}
workers().forEach((worker) => worker!.kill());
const forceStopHandle = setTimeout(
this.forceStop,
this.configuration.stopGracetimeMs
);
this.stoppingPromise = new Promise((resolve) =>
cluster.on('exit', () => {
if (workers().length <= 0) {
resolve();
clearTimeout(forceStopHandle);
this.stoppingPromise = undefined;
}
})
);
return this.stoppingPromise;
};
private forceStop = () =>
workers().forEach((worker) => worker!.kill('SIGKILL'));
}