When deploying Node.js applications with PM2, it’s important to understand how cluster mode, instances, and CPU cores interact — especially when you’re combining multiple apps like a Next.js frontend and a Feathers.js backend.
This post breaks down the core concepts and lessons learned from setting up next-dna
and feathers-dna
with PM2 cluster mode.
Instance in PM2 refers to a copy of your app.
Worker is the actual process that runs an instance.
Cluster mode allows PM2 to spawn multiple Node.js processes behind a single port using the built-in cluster module.
Ideally, you run one instance per CPU core, so the workload is evenly distributed.
You can check your available cores with:
nproc
If you set:
instances: "max"
PM2 will match your number of CPU cores.
Even if you only run 1 instance, using cluster mode still works — and may even be better than fork mode.
Why?
Cluster mode sets up a master process that manages workers.
It keeps the option open to scale later without changing architecture.
PM2 handles restarts and monitoring more cleanly in cluster mode.
So yes — cluster mode with 1 instance is valid and sometimes preferable.
⚠️ Important Note:
Cluster mode with only 1 instance does not support true zero-downtime reloads.
During a reload, that single worker must exit before a new one is started, resulting in a short downtime window (you may briefly see 503 errors).
To enable zero-downtime reloads, you need at least 2 instances.
You can run multiple instances on a single core — but you shouldn’t.
What happens:
The OS time-slices the CPU across all your workers.
This leads to context switching, where the CPU keeps switching between processes.
That can introduce latency and reduce throughput.
It’s not catastrophic in small apps, but for production workloads, you’ll want at most one worker per core.
If you go overboard (e.g., 4 workers on 2 cores):
You’re not magically getting 4× performance.
Instead, your app can become slower due to CPU thrashing.
If your app is also I/O-bound (e.g., hitting a DB), performance may degrade even further.
📌 Tip: Use instances: os.cpus().length
or simply instances: "max"
for optimal performance.
By default, if you naively start Feathers.js in cluster mode using:
app.listen(port);
Each worker will try to bind to the same port — and crash with:
EADDRINUSE: address already in use
You need to tweak your startup logic:
const startServer = async () => {
await app.setup();
if (process.env.NODE_APP_INSTANCE === undefined || process.env.NODE_APP_INSTANCE === '0') {
await app.listen(port);
logger.info(`Feathers app listening on port ${port}`);
} else {
logger.info(`Worker ${process.env.NODE_APP_INSTANCE} initialized`);
}
};
startServer();
This ensures only one worker binds to the port, and others are still initialized properly to handle requests routed by PM2.
If your frontend (Next.js) is running 2 instances, and your backend (Feathers.js) is only running 1 (e.g., fork mode):
You may see errors like:
socket hang up
Or long response delays, as the single Feathers.js worker becomes a bottleneck.
Why?
Next.js routes API calls to Feathers.js.
If there are more frontend workers than backend, you’ll have idle frontend capacity, but backend saturation.
💡 Matching instance counts between next-dna
and feathers-dna
reduces the risk of these issues.
After much testing, the following setup works stably:
// pm2 config
instances: 1,
exec_mode: "cluster",
For both:
next-dna
(Next.js)
feathers-dna
(Feathers.js)
This avoids port binding issues, avoids overloading CPU, and keeps things simple — while still using cluster mode for consistency and future scalability.
🔁 For zero-downtime reloads, bump to
instances: 2
on both apps.
✅ Use cluster mode, even for 1 instance — it’s cleaner.
🧠 Match instances to CPU cores — don’t oversubscribe.
⚙️ Feathers.js needs a startup guard for port binding.
🔁 1 instance ≠ zero-downtime — for that, use 2+ instances.
📶 Match instance counts between frontend and backend for stability.