Je Node.js applicatie draait prima op je dev-machine, maar zodra er duizenden gebruikers tegelijk inloggen, staat één proces op één core te puffen terwijl de rest van je server niets doet. Daar komen clustering en scaling om de hoek kijken: technieken om je Node.js app te verspreiden over meerdere cores en meerdere servers, zodat je applicatie meegroeit met je verkeer.
In dit artikel leer je hoe Node.js van nature single-threaded werkt, hoe je met het cluster-module meerdere workers start, en hoe je vervolgens horizontaal schaalt met load balancers en orchestrators. We bouwen voort op concepten uit eerdere artikelen zoals de event loop en performance tuning.
Waarom clustering überhaupt nodig is
Node.js draait standaard in één enkel proces op één CPU-core. Dat is prima voor I/O-bound workloads dankzij de event loop, maar zodra je server meerdere cores heeft (en dat zijn ze tegenwoordig allemaal), laat je capaciteit liggen.
Een klassiek voorbeeld: je hebt een VPS met 4 cores en 8 GB RAM. Zonder clustering gebruikt Node maximaal 1 core. De andere 3 staan idle. Dat is letterlijk 75% van je rekencapaciteit die je niet inzet.
Clustering lost dit op door meerdere Node.js processen te draaien die samen requests afhandelen. Elke worker krijgt zijn eigen event loop en zijn eigen stukje CPU.
Vertical vs horizontal scaling
Scaling kent twee smaken:
- Vertical scaling: één server krachtiger maken (meer cores, meer RAM). Clustering valt hieronder.
- Horizontal scaling: meerdere servers parallel draaien, met een load balancer ervoor.
In de praktijk combineer je beide: je cluster draait op elke server, en je zet meerdere servers achter een load balancer.
Het Node.js cluster-module
Node.js heeft een ingebouwd cluster-module dat een master-worker architectuur biedt. De master forkt child-processen (workers) die allemaal dezelfde poort delen.
const cluster = require('node:cluster');
const http = require('node:http');
const os = require('node:os');
const numCPUs = os.availableParallelism();
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`Worker ${worker.process.pid} died, respawning`);
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end(`Handled by worker ${process.pid}\n`);
}).listen(3000);
console.log(`Worker ${process.pid} started`);
}
Wat gebeurt hier:
- De primary (master) proces start en forkt één worker per CPU-core.
- Elke worker start een HTTP-server op poort 3000.
- De OS-kernel (of Node zelf op Windows) verdeelt inkomende connecties round-robin over de workers.
- Als een worker crasht, start de primary automatisch een nieuwe op.
Round-robin en sticky sessions
Op Linux gebruikt Node standaard round-robin scheduling. Op Windows doet de OS dit via shared socket handles. Dat werkt prima voor stateless HTTP, maar bij WebSockets of sticky sessions heb je een extra laag nodig (daarover later meer).
Worker threads vs cluster
Node.js heeft ook worker_threads, wat soms verwarrend is. Het verschil:
- Cluster: meerdere volledige Node.js processen, elk met eigen geheugen. Geschikt voor HTTP-servers.
- Worker threads: threads binnen één proces, gedeeld geheugen via
SharedArrayBuffer. Geschikt voor CPU-intensieve berekeningen.
Voor het schalen van een web-API kies je vrijwel altijd voor clustering. Voor zware CPU-taken zoals image processing of encryptie is een worker thread pool vaak beter. Zie onze gids over background jobs en workers voor meer context.
PM2: clustering in productie
Het cluster-module handmatig beheren is leerzaam, maar in productie wil je een process manager die crashes, logs, deploys en zero-downtime reloads regelt. PM2 is hiervoor de standaardtool.
npm install -g pm2
pm2 start app.js -i max --name api
pm2 save
pm2 startup
De -i max flag start één worker per beschikbare core. Je kunt ook een vast aantal opgeven (-i 4) of -i 0 voor fork-mode zonder clustering.
Zero-downtime reloads
PM2 kan workers één voor één vervangen tijdens een deploy:
pm2 reload api
Hiermee start PM2 een nieuwe worker, wacht tot die luistert, en sluit daarna pas de oude af. Zo heb je geen enkele gedropte request. Combineer dit met een goede graceful shutdown in je applicatie zodat lopende requests kunnen afronden.
Ecosystem-bestand
Voor meer controle gebruik je een ecosystem.config.js:
module.exports = {
apps: [{
name: 'api',
script: './dist/server.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '500M',
env_production: {
NODE_ENV: 'production',
PORT: 3000
}
}]
};
max_memory_restart is handig: PM2 restart een worker automatisch als die boven een geheugenlimiet komt. Nuttig als een memory leak sluipt.
State delen tussen workers
Dit is waar veel teams struikelen. Elke worker heeft eigen geheugen. Alles wat je in-process cachet of in een globale variabele zet, is dus niet beschikbaar voor andere workers.
Praktische gevolgen:
- In-memory sessies werken niet meer. Een gebruiker logt in op worker A, zijn volgende request komt bij worker B, en die weet van niets.
- In-memory caches zijn per-worker. Als elke worker zijn eigen Redis-less cache opbouwt, verspil je geheugen en krijg je inconsistenties.
- Rate limiting telt per worker. Een limiet van 100 requests per minuut wordt effectief 100 × aantal workers.
Redis als shared store
De oplossing is bijna altijd: externe state. Redis is hiervoor de go-to:
import { createClient } from 'redis';
import session from 'express-session';
import RedisStore from 'connect-redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
Meer over sessies en JWT lees je in authentication in Node.js. Voor database-patronen is werken met databases een goede vervolgstap.
Horizontal scaling: meerdere servers
Zodra één server, zelfs volledig geclusterd, de load niet meer aankan, ga je horizontaal. Dat betekent meerdere servers parallel, met een load balancer die verkeer verdeelt.
Load balancers
Populaire keuzes:
- Nginx: lichtgewicht, veel gebruikt als reverse proxy.
- HAProxy: sterk in TCP en geavanceerde routing.
- Cloud load balancers: AWS ALB, Google Cloud Load Balancer, Cloudflare Load Balancing.
Een minimale Nginx-configuratie voor twee Node.js servers:
upstream nodejs_backend {
least_conn;
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}
server {
listen 80;
location / {
proxy_pass http://nodejs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
}
De least_conn strategie stuurt nieuwe connecties naar de server met het minste actieve werk. Voor stateless HTTP is round_robin ook prima.
Sticky sessions voor WebSockets
Bij WebSockets of long-polling moet een client consistent bij dezelfde server blijven. Anders verlies je de verbinding. Dat regel je met sticky sessions (ip_hash in Nginx) óf door een Redis pub/sub adapter te gebruiken die events tussen servers synchroniseert.
Containers en orchestrators
Moderne deployments draaien vaak in containers. Docker + Kubernetes is de de facto standaard voor grotere setups.
De interessante verschuiving: in Kubernetes cluster je meestal niet binnen een container. In plaats daarvan draai je één Node.js proces per container (pod), en laat je Kubernetes het aantal pods schalen op basis van CPU-gebruik.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Hiermee schaalt Kubernetes automatisch tussen 3 en 20 pods. Meer dan genoeg voor de meeste webapps. De Kubernetes documentatie over autoscaling legt dit uitgebreid uit.
Stateless architectuur als fundament
Wil je goed kunnen schalen, dan moet je app stateless zijn. Dat betekent:
- Geen bestanden opslaan op de lokale disk (gebruik S3 of equivalent).
- Geen sessies in-memory (gebruik Redis).
- Geen in-memory rate limiters (gebruik Redis of een gateway).
- Geen cron jobs per server (gebruik een centrale scheduler of locking).
- Configuratie via environment variables, niet via lokale files.
De Twelve-Factor App methodiek is een uitstekende checklist voor dit soort ontwerp. Als je app statelessness respecteert, kun je workers gewoon toevoegen of weghalen zonder dat er iets breekt.
Monitoring en observability
Schalen zonder monitoring is gokken. Minimaal wil je zien:
- Event loop lag per worker, als die stijgt, is je proces overbelast.
- Memory per worker, leaks zijn lastiger op te sporen in een cluster.
- Request rate en latency per server en per worker.
- Autoscaling events, wanneer schaalde de orchestrator op of af?
Tools zoals Prometheus, Grafana, Datadog of New Relic geven je hier grip op. Combineer met goede logging (bijvoorbeeld via middleware patterns) zodat je per request kunt herleiden welke worker wat deed.
Wanneer moet je eigenlijk schalen?
Pragmatische vuistregel:
- Begin met één proces. Meet de werkelijke load.
- Clustering toevoegen zodra CPU structureel boven ~70% komt.
- Horizontal scaling zodra één volledig geclusterde server niet meer volstaat, óf je redundantie nodig hebt.
- Autoscaling wanneer je load sterk varieert (bijvoorbeeld piekverkeer rond campagnes).
Te vroeg schalen verhoogt complexiteit zonder voordeel. Te laat schalen kost je uptime en klanten. Meten is hier cruciaal.
Veelgestelde vragen
Wat is clustering in Node.js?
Clustering is een techniek waarbij je meerdere Node.js processen draait op één server, zodat je alle CPU-cores benut. Het cluster-module zorgt dat requests verdeeld worden over de workers, waardoor je applicatie meer verkeer aankan.
Wat is het verschil tussen clustering en scaling?
Clustering verdeelt werk over meerdere processen op dezelfde machine (vertical scaling). Scaling, en dan vooral horizontal scaling, verdeelt werk over meerdere servers. Ze vullen elkaar aan: eerst cluster je per server, dan schaal je horizontaal.
Moet ik clustering zelf implementeren of PM2 gebruiken?
Voor productie is PM2 of een container-orchestrator zoals Kubernetes meestal de betere keuze. Deze tools regelen automatisch restarts, monitoring en load balancing. Het ingebouwde cluster-module is nuttig om te begrijpen hoe het werkt.
Hoe deel ik state tussen clustered Node.js processen?
Workers delen geen geheugen. Gebruik externe stores zoals Redis voor sessies, caches en pub/sub. Voor WebSockets heb je een adapter nodig (bijvoorbeeld Redis adapter voor Socket.IO) zodat events tussen workers gesynchroniseerd worden.
Wanneer moet ik horizontaal gaan schalen?
Zodra één server ook na clustering de load niet meer aankan, of als je downtime bij deploys en uitval wil minimaliseren. Horizontal scaling geeft je redundantie en flexibiliteit, maar vraagt wel om stateless architectuur en een goede load balancer.