Node.js Scaling in 2026: What Actually Breaks First (and How to Fix It)

Node.js usually breaks first on synchronous work blocking the event loop, then on one process running on a multi-core box, then on slow database queries. Fix the blocking code first, cluster across your cores second, and tune queries and connection pooling third. Scaling the server before fixing those just multiplies the problem.
Part of the complete web development guide.
On this page
- Start by proving you have a scaling problem
- The three things that actually break first
- 1. Synchronous work on the event loop
- 2. Running on one core
- 3. Connection pools that don’t match your process count
- What I’d actually do in order
- FAQ
- Do I need worker threads at all if I’m not doing CPU-heavy work?
- Is the cluster module still worth using in 2026?
- How many database connections should each Node process open?
Most Node.js performance problems I get called about aren’t Node problems at all. They’re a single slow database query, a synchronous call blocking the event loop, or a service running one process on a box with eight cores. Node scales fine. The default setup just doesn’t. Here’s what actually breaks first and what to do about it, in the order I’d fix it.
Start by proving you have a scaling problem
Before you touch anything, measure. Node has built-in tools that most people never open, and they tell you more than any dashboard.
# Watch the event loop. High lag means something is blocking it.
node --trace-event-categories v8,node,node.async_hooks app.js
# Or just check it from inside the app
const { monitorEventLoopDelay } = require('node:perf_hooks');
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
console.log('p99 event loop delay (ms):', (h.percentile(99) / 1e6).toFixed(1));
}, 5000);
If that p99 number is consistently above 50ms under normal load, you have an event loop problem, and no amount of horizontal scaling will fix bad code. If it’s low and your latency is still bad, you’re probably waiting on I/O somewhere else: database, an upstream API, disk. Those are different fixes.
The three things that actually break first
1. Synchronous work on the event loop
Node runs your JavaScript on one thread. Anything CPU-heavy (JSON parsing huge payloads, image resizing, crypto, regex on large strings) blocks everything else while it runs. I’ve seen a single JSON.parse of a 40MB payload add two seconds of latency to every other request on the process.
Fix it one of two ways. Move it off the main thread with worker threads:
// resize-worker.js
const { parentPort, workerData } = require('node:worker_threads');
const sharp = require('sharp');
sharp(workerData.buffer)
.resize(800)
.toBuffer()
.then(out => parentPort.postMessage(out));
// main.js
const { Worker } = require('node:worker_threads');
function resize(buffer) {
return new Promise((resolve, reject) => {
const w = new Worker('./resize-worker.js', { workerData: { buffer } });
w.once('message', resolve);
w.once('error', reject);
});
}
Or push it to a queue and let a separate worker service handle it. For anything that takes more than a few milliseconds, the queue is usually the better call because it also survives a deploy.
2. Running on one core
Node uses one CPU core by default. If your server has eight, you’re leaving seven idle. The cluster module fixes this by forking one process per core, all sharing the same port:
const cluster = require('node:cluster');
const os = require('node:os');
const http = require('node:http');
if (cluster.isPrimary) {
const cpus = os.availableParallelism();
console.log(`primary ${process.pid} forking ${cpus} workers`);
for (let i = 0; i < cpus; i++) cluster.fork();
cluster.on('exit', (worker) => {
console.error(`worker ${worker.process.pid} died, replacing`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.end(`handled by ${process.pid}n`);
}).listen(3000);
}
In practice you rarely write this by hand anymore. PM2 does it for you with pm2 start app.js -i max, and if you’re on Kubernetes you just run more pods and let the orchestrator handle it. Either way, the point is the same: one process per core, minimum.
3. Connection pools that don’t match your process count
This is the one that bites people after they scale up. If you run 8 Node processes and each one opens a pool of 10 database connections, that’s 80 connections hitting a Postgres box that might only allow 100. You’ll get connection errors under load and spend an afternoon blaming the database.
// Size the pool per process, not per app
const { Pool } = require('pg');
const pool = new Pool({
max: Math.max(2, Math.floor(TOTAL_DB_CONNECTIONS / WORKER_COUNT)),
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
});
Do the math before you scale. Total connections across all processes should stay comfortably under what your database allows, and under what it can actually serve well. More connections is not more throughput past a certain point.
What I’d actually do in order
- Measure event loop delay and p99 latency under real load. Don’t guess.
- Find synchronous hot paths and move them to worker threads or a queue.
- Run one process per core (PM2, cluster, or more pods).
- Re-check your database connection math now that you have more processes.
- Only then look at caching, CDNs, and read replicas.
Most of the Node work I do for clients around Commack and the rest of Long Island lands somewhere in that list. It’s rarely exotic. The systems that hold up are the ones where somebody measured first and fixed the actual bottleneck instead of adding servers and hoping. If you’re building something new and want the architecture right from the start, that’s a big part of what custom web development looks like in practice: pick the boring, correct setup before you need it, not after it falls over.
FAQ
Do I need worker threads at all if I’m not doing CPU-heavy work?
Probably not. Most web apps are I/O bound, which Node handles well on a single thread. Worker threads earn their keep for image processing, PDF generation, compression, crypto, and large payload parsing. If none of that describes your app, skip them.
Is the cluster module still worth using in 2026?
Yes, but you usually won’t call it directly. PM2 in cluster mode or running multiple containers with a shared load balancer covers the same ground with less code to maintain. Use the raw module when you want tight control over forking and restart behavior.
How many database connections should each Node process open?
Start with 5 to 10 per process and watch your database’s connection count and wait times. Divide your database’s safe maximum by the number of processes you run. If you’re hitting the ceiling, add a pooler like PgBouncer before you add more app servers.


