// bun-serve-spawn-cluster.js
import { spawn } from "bun";
const method = process.argv[2];
if (['port', 'socket'].includes(method) !== true) {
console.error(`usage: bun ${process.argv[1]} [port,socket]`);
process.exit(1);
}
const cpus = 4; // Number of CPU cores
const buns = new Array(cpus);
for (let i = 0; i < cpus; i++) {
buns[i] = spawn({
cmd: ["bun", "./bun-serve-worker.js", method, i],
stdout: "inherit",
stderr: "inherit",
stdin: "inherit",
});
}
function kill() {
for (const bun of buns) {
bun.kill();
}
}
process.on("SIGINT", kill);
process.on("exit", kill);
// bun-serve-worker.js
import { serve } from "bun";
const method = process.argv[2]; // port or socket
const id = process.argv[3] ?? process.env.WORKER_NUM; // worker id
if (['port', 'socket'].includes(method) !== true || id === undefined) {
console.error(`usage: bun ${process.argv[1]} [port,socket] [1,2,3,4]`);
process.exit(1);
}
const serveOpts = {
// hostname: '127.0.0.1',
// port: 8080,
// unix: '/run/bun-serve-worker.sock',
development: false,
// Share the same port across multiple processes
// This is the important part!
reusePort: true,
async fetch(request) {
return new Response("Hello from Bun #" + id + "!\n");
}
};
if (method === 'port') {
serveOpts.hostname = '127.0.0.1';
serveOpts.port = 8080;
console.log(`bun-server-worker [${id}] test via: curl http://127.0.0.1:8080`);
} else if (method === 'socket') {
serveOpts.unix = '/run/bun-serve-worker.sock';
console.log(`bun-server-worker [${id}] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080`);
}
serve(serveOpts);
Terminal 1: $ bun bun-serve-spawn-cluster.js port
Terminal 2: $ for x in 1 2 3 4 5 6 7 8; do curl http://127.0.0.1:8080; done
Terminal 2: notice that different workers respond to the request
Terminal 1: press CTRL+C to kill
Terminal 1: $ bun bun-serve-spawn-cluster.js socket
Terminal 2: $ for x in 1 2 3 4 5 6 7 8; do curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080; done
Terminal 2: notice that only one worker responds to the request
Terminal 1: press CTRL+C to kill
In Terminal 1, you can also test out bun-serve-node-cluster.js, which uses the node:cluster module instead, which exhibits the same behavior.
// bun-serve-node-cluster.js
import cluster from 'node:cluster';
const method = process.argv[2];
if (['port', 'socket'].includes(method) !== true) {
console.error(`usage: bun ${process.argv[1]} [port,socket]`);
process.exit(1);
}
const cpus = 4; // Number of CPU cores
cluster.setupPrimary({
exec: './bun-serve-worker.js',
args: [method]
});
for (let i = 0; i < cpus; i++) {
cluster.fork({ WORKER_NUM: i });
}
What is the expected behavior?
Regardless of listening on a tcp port or unix domain socket, it should round-robin (or at least distribute somehow) the requests, similar to the following:
bun-server-worker [2] test via: curl http://127.0.0.1:8080
bun-server-worker [3] test via: curl http://127.0.0.1:8080
bun-server-worker [0] test via: curl http://127.0.0.1:8080
bun-server-worker [1] test via: curl http://127.0.0.1:8080
Hello from Bun #3!
Hello from Bun #1!
Hello from Bun #0!
Hello from Bun #0!
Hello from Bun #2!
Hello from Bun #1!
Hello from Bun #2!
Hello from Bun #3!
What do you see instead?
When listening on a unix domain socket, only one worker, likely the last one executed during the spawn race, answers all of the requests:
bun-server-worker [1] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
bun-server-worker [0] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
bun-server-worker [3] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
bun-server-worker [2] test via: curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080
During worker spawn, sometimes it will throw EADDRINUSE: Address already in use for the unix domain socket path, which is a pretty good indicator that Bun isn't handling this use case. Just re-run the command until it doesn't error, so you can confirm the findings.
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Hello from Bun #2!
Additional information
I consider this a bug, because Node.js works as expected ( distributes the requests to workers, regardless of listening on tcp port or unix domain socket) when using node:cluster.
What version of Bun is running?
1.1.26+0a37423ba
What platform is your computer?
Linux 6.10.6-x64v3-xanmod1 x86_64 x86_64
What steps can reproduce the bug?
Terminal 1:
$ bun bun-serve-spawn-cluster.js port
Terminal 2:$ for x in 1 2 3 4 5 6 7 8; do curl http://127.0.0.1:8080; done
Terminal 2:notice that different workers respond to the request
Terminal 1:press CTRL+C to kill
Terminal 1:
$ bun bun-serve-spawn-cluster.js socket
Terminal 2:$ for x in 1 2 3 4 5 6 7 8; do curl --unix-socket /run/bun-serve-worker.sock http://127.0.0.1:8080; done
Terminal 2:notice that only one worker responds to the request
Terminal 1:press CTRL+C to kill
In Terminal 1, you can also test out
bun-serve-node-cluster.js
, which uses thenode:cluster
module instead, which exhibits the same behavior.What is the expected behavior?
Regardless of listening on a tcp port or unix domain socket, it should round-robin (or at least distribute somehow) the requests, similar to the following:
What do you see instead?
When listening on a unix domain socket, only one worker, likely the last one executed during the spawn race, answers all of the requests:
EADDRINUSE: Address already in use
for the unix domain socket path, which is a pretty good indicator that Bun isn't handling this use case. Just re-run the command until it doesn't error, so you can confirm the findings.Additional information
I consider this a bug, because Node.js works as expected ( distributes the requests to workers, regardless of listening on tcp port or unix domain socket) when using
node:cluster
.