# Node.js Performance: Event Loop, Clustering en Optimalisatie in 2026 > Node.js-prestatieoptimalisatie via event loop management, clusteringstrategieën en worker threads. Praktische patronen voor high-throughput Node.js-applicaties in 2026. - Published: 2026-04-25 - Updated: 2026-04-25 - Author: SharpSkill - Tags: node.js, performance, event-loop, clustering, optimalisatie, worker-threads - Reading time: 9 min --- Prestatieoptimalisatie van Node.js begint met het begrijpen van de event loop — het single-threaded mechanisme dat alle asynchrone I/O-operaties afhandelt. In productiesystemen met Node.js 22 LTS of Node.js 24 blijft gebrekkig event loop management de voornaamste oorzaak van latentiepieken, verbroken verbindingen en cascadefouten onder belasting. Deze gids behandelt de drie pijlers van Node.js-performance: event loop internals, multi-core schaling met clusters en worker threads, en beproefde optimalisatiepatronen voor systemen met hoge doorvoer. > **Kernpunt** > > De event loop verwerkt callbacks in een strikte fasevolgorde: timers, pending callbacks, idle/prepare, poll, check en close callbacks. Het blokkeren van één enkele fase legt de gehele applicatie stil. Monitoring van event loop-gebruik met `performance.eventLoopUtilization()` is het meest effectieve diagnostische hulpmiddel voor Node.js-prestatieproblemen. ## Hoe de Node.js Event Loop verzoeken verwerkt De event loop is geen eenvoudige wachtrij. Het werkt via zes afzonderlijke fasen, elk verantwoordelijk voor een specifieke categorie callbacks. Het begrijpen van deze fase-architectuur verklaart waarom bepaalde patronen latentie veroorzaken en andere niet. ```javascript // event-loop-phases.js // Demonstrating phase execution order const fs = require('fs'); // Phase 1: Timers — executes setTimeout/setInterval callbacks setTimeout(() => console.log('1. Timer phase'), 0); // Phase 4: Poll — executes I/O callbacks fs.readFile(__filename, () => { console.log('2. Poll phase (I/O callback)'); // Phase 5: Check — executes setImmediate callbacks setImmediate(() => console.log('3. Check phase (setImmediate)')); // Phase 1 again: Timer scheduled from within I/O setTimeout(() => console.log('4. Timer phase (from I/O)'), 0); }); // Microtask — runs between every phase transition Promise.resolve().then(() => console.log('Microtask: Promise')); process.nextTick(() => console.log('Microtask: nextTick')); ``` De uitvoervolgorde onthult de faseprioriteit: `nextTick` wordt uitgevoerd vóór promises, promises vóór timers, en `setImmediate` wordt altijd geactiveerd na I/O-callbacks in de check-fase. Deze volgorde is cruciaal bij het ontwerpen van latentiegevoelige request handlers. ## Event Loop-blokkades detecteren in productie Een geblokkeerde event loop manifesteert zich als stijgende p99-latentie, ruim voordat de gemiddelde responstijden verslechteren. De ingebouwde `performance.eventLoopUtilization()` API, stabiel sinds Node.js 16, biedt het meest betrouwbare detectiemechanisme. ```javascript // event-loop-monitor.js // Production-grade event loop monitoring const { performance, monitorEventLoopDelay } = require('perf_hooks'); // High-resolution event loop delay histogram const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); // Track utilization over intervals let previous = performance.eventLoopUtilization(); setInterval(() => { const current = performance.eventLoopUtilization(previous); previous = performance.eventLoopUtilization(); const metrics = { // Ratio of time the loop spent active vs idle (0-1) utilization: current.utilization.toFixed(3), // Delay percentiles in milliseconds p50: (histogram.percentile(50) / 1e6).toFixed(2), p99: (histogram.percentile(99) / 1e6).toFixed(2), max: (histogram.max / 1e6).toFixed(2), }; // Alert when utilization exceeds 70% or p99 > 100ms if (current.utilization > 0.7 || histogram.percentile(99) > 100e6) { console.warn('EVENT_LOOP_SATURATED', metrics); } histogram.reset(); }, 5000); ``` Een gebruik boven 0,7 (70%) signaleert dat de event loop meer tijd besteedt aan het uitvoeren van callbacks dan aan het wachten op I/O. Boven deze drempel beginnen inkomende verbindingen zich op te hopen en stijgen de staartlatenties exponentieel. ## Veelvoorkomende Event Loop-blokkers en hun oplossingen Drie patronen zijn verantwoordelijk voor het merendeel van event loop-blokkades in Node.js-productieapplicaties: synchrone JSON-operaties op grote payloads, CPU-intensieve berekeningen in request handlers, en onbegrensde reguliere expressies. ```javascript // blocking-patterns.js // Anti-patterns and their solutions // PROBLEM: JSON.parse blocks on large payloads const largePayload = Buffer.alloc(50 * 1024 * 1024); // 50MB // JSON.parse(largePayload.toString()); // Blocks event loop 200-500ms // SOLUTION: Stream-parse large JSON with a streaming parser const { Transform } = require('stream'); const JSONStream = require('jsonstream2'); function processLargeJSON(readableStream) { return new Promise((resolve, reject) => { const results = []; readableStream .pipe(JSONStream.parse('items.*')) // Stream-parse array items .on('data', (item) => results.push(item)) .on('end', () => resolve(results)) .on('error', reject); }); } // PROBLEM: Synchronous crypto in request path // const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512'); // SOLUTION: Use async variant const crypto = require('crypto'); async function hashPassword(password, salt) { return new Promise((resolve, reject) => { crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, key) => { if (err) reject(err); else resolve(key.toString('hex')); }); }); } ``` De asynchrone variant van `pbkdf2` verplaatst het CPU-werk naar de libuv-threadpool, waardoor de event loop vrij blijft om andere verzoeken te verwerken. Alleen al deze aanpassing verlaagde de p99-latentie van 900 ms naar 120 ms in een gedocumenteerd productie-incident bij een fintech-bedrijf. ## Node.js schalen over CPU-cores met de Cluster-module Een enkel Node.js-proces gebruikt één CPU-core. Op een productieserver met 16 cores betekent dit dat 93% van de beschikbare rekenkracht onbenut blijft. De ingebouwde `cluster`-module spawnt workerprocessen die een enkele poort delen en inkomende verbindingen verdelen over alle cores. ```javascript // cluster-setup.js // Production clustering with graceful shutdown const cluster = require('cluster'); const os = require('os'); const process = require('process'); const WORKER_COUNT = parseInt(process.env.WORKERS) || os.cpus().length; if (cluster.isPrimary) { console.log(`Primary ${process.pid} starting ${WORKER_COUNT} workers`); // Fork workers for each CPU core for (let i = 0; i < WORKER_COUNT; i++) { cluster.fork(); } // Restart crashed workers automatically cluster.on('exit', (worker, code, signal) => { if (!worker.exitedAfterDisconnect) { console.error(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`); cluster.fork(); } }); // Graceful shutdown on SIGTERM process.on('SIGTERM', () => { console.log('Primary received SIGTERM. Shutting down workers...'); for (const id in cluster.workers) { cluster.workers[id].disconnect(); } }); } else { // Worker process — start the actual HTTP server const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200); res.end(`Handled by worker ${process.pid}\n`); }); server.listen(3000, () => { console.log(`Worker ${process.pid} listening on port 3000`); }); // Graceful shutdown for individual worker process.on('SIGTERM', () => { server.close(() => process.exit(0)); }); } ``` Elke worker draait in een eigen V8-isolate met gescheiden geheugen. Workers delen geen state — sessiegegevens, caches en applicatiestatus moeten in een externe store zoals Redis of PostgreSQL worden opgeslagen. Deze isolatie biedt ook fouttolerantie: een crash in één worker heeft geen effect op de andere. ## Worker Threads voor CPU-intensieve taken De `cluster`-module dupliceert het gehele proces. Worker threads, geïntroduceerd in Node.js 10 en stabiel sinds Node.js 12, voeren JavaScript uit in parallelle threads binnen hetzelfde proces. Ze delen geheugen via `SharedArrayBuffer` en dragen gegevens over via structured cloning. Het onderscheid is belangrijk: gebruik clusters voor het schalen van I/O-gebonden HTTP-servers over cores, en worker threads voor het offloaden van CPU-gebonden operaties van de event loop. ```javascript // worker-pool.js // Reusable worker thread pool for CPU tasks const { Worker } = require('worker_threads'); const os = require('os'); class WorkerPool { constructor(workerScript, poolSize = os.cpus().length) { this.workers = []; this.queue = []; for (let i = 0; i < poolSize; i++) { this.workers.push({ busy: false, worker: new Worker(workerScript) }); } } execute(taskData) { return new Promise((resolve, reject) => { const available = this.workers.find(w => !w.busy); if (available) { this._runTask(available, taskData, resolve, reject); } else { // Queue task until a worker is free this.queue.push({ taskData, resolve, reject }); } }); } _runTask(entry, taskData, resolve, reject) { entry.busy = true; entry.worker.postMessage(taskData); const onMessage = (result) => { entry.busy = false; cleanup(); resolve(result); this._processQueue(); }; const onError = (err) => { entry.busy = false; cleanup(); reject(err); this._processQueue(); }; const cleanup = () => { entry.worker.removeListener('message', onMessage); entry.worker.removeListener('error', onError); }; entry.worker.on('message', onMessage); entry.worker.on('error', onError); } _processQueue() { if (this.queue.length === 0) return; const available = this.workers.find(w => !w.busy); if (available) { const { taskData, resolve, reject } = this.queue.shift(); this._runTask(available, taskData, resolve, reject); } } } module.exports = { WorkerPool }; ``` ```javascript // image-worker.js // Worker thread for CPU-intensive image processing const { parentPort } = require('worker_threads'); const sharp = require('sharp'); parentPort.on('message', async ({ inputPath, width, height }) => { const result = await sharp(inputPath) .resize(width, height) .webp({ quality: 80 }) .toBuffer(); parentPort.postMessage({ size: result.length, buffer: result }); }); ``` Een worker pool start threads vooraf bij het opstarten van de applicatie en hergebruikt ze over meerdere verzoeken. Dit elimineert de 30-50 ms overhead voor het aanmaken van een nieuwe worker thread per verzoek. Voor beeldverwerking, PDF-generatie of datatransformatie houdt dit patroon de latentie van de hoofd-event loop onder 5 ms, zelfs onder aanhoudende CPU-belasting. ## Geheugenoptimalisatie en Garbage Collection tuning V8 verdeelt de heap in de young generation (kortlevende objecten) en de old generation (langlevende objecten). De meeste prestatieproblemen ontstaan door excessieve allocaties in de young generation, die frequente minor GC-pauzes veroorzaken, of door geheugenlekken die de old generation laten groeien totdat major GC-pauzes zichtbare latentiepieken veroorzaken. ```javascript // memory-optimization.js // Patterns that reduce GC pressure // ANTI-PATTERN: Creating objects in hot loops function processItemsBad(items) { return items.map(item => ({ id: item.id, name: item.name.trim(), score: calculateScore(item), // New object per iteration metadata: { processed: true, timestamp: Date.now() } })); } // OPTIMIZED: Reuse buffers and minimize allocations const reusableBuffer = Buffer.alloc(4096); function processItemsGood(items, output) { // Reuse the output array instead of creating new one output.length = 0; for (let i = 0; i < items.length; i++) { // Mutate in place when safe to do so output.push(items[i].id); } return output; } // Monitor heap usage for leak detection function checkMemory() { const used = process.memoryUsage(); return { heapUsedMB: Math.round(used.heapUsed / 1024 / 1024), heapTotalMB: Math.round(used.heapTotal / 1024 / 1024), externalMB: Math.round(used.external / 1024 / 1024), rsssMB: Math.round(used.rss / 1024 / 1024), }; } // V8 flags for production GC tuning // node --max-old-space-size=4096 --max-semi-space-size=128 app.js // --max-old-space-size: Set old generation limit (default ~1.7GB) // --max-semi-space-size: Increase young generation (default 16MB) ``` Het verhogen van `--max-semi-space-size` van de standaard 16 MB naar 64-128 MB vermindert de minor GC-frequentie voor applicaties met hoge allocatiesnelheden. Hierbij wordt geheugen geruild tegen lagere GC-pauzefrequentie — een waardevolle afweging op servers met 8 GB+ RAM. ## Productiemonitoring met OpenTelemetry in Node.js 24 OpenTelemetry is in 2026 de standaard instrumentatieframework voor Node.js geworden. De Node.js 24-runtime bevat verbeterde profilingondersteuning met inline cache-gegevens in CPU-profielen, waardoor prestatieanalyse aanzienlijk nauwkeuriger wordt. ```javascript // otel-setup.js // OpenTelemetry setup for Node.js performance monitoring const { NodeSDK } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); const sdk = new NodeSDK({ metricReader: new PrometheusExporter({ port: 9464 }), instrumentations: [ getNodeAutoInstrumentations({ // Instrument HTTP, Express, DNS, fs, and more '@opentelemetry/instrumentation-fs': { enabled: false }, // Too noisy }), ], }); sdk.start(); // Custom event loop lag metric const { metrics } = require('@opentelemetry/api'); const meter = metrics.getMeter('app'); const eventLoopLag = meter.createHistogram('nodejs.event_loop.lag', { description: 'Event loop lag in milliseconds', unit: 'ms', }); // Report event loop lag every second const { monitorEventLoopDelay } = require('perf_hooks'); const h = monitorEventLoopDelay({ resolution: 10 }); h.enable(); setInterval(() => { eventLoopLag.record(h.percentile(99) / 1e6); h.reset(); }, 1000); ``` Deze configuratie exporteert event loop lag, HTTP-verzoekduur en DNS-resolutietijd als Prometheus-metrics. Het instellen van alerts op event loop lag p99 > 100 ms detecteert verslechtering voordat gebruikers het merken. > **Cluster + Worker Thread valkuil** > > Elke cluster worker krijgt een eigen libuv-threadpool (standaard 4 threads). Op een 16-core machine met 16 cluster workers betekent dat 64 libuv-threads die strijden om CPU-tijd. Stel `UV_THREADPOOL_SIZE` in op 2-4 per worker in geclusterde omgevingen, en reserveer cores voor worker threads die CPU-taken afhandelen. ## Kiezen tussen Clusters, Worker Threads en externe schaling De juiste schalingsstrategie hangt af van het workloadprofiel. Een beslissingsmatrix op basis van echte productiepatronen: | Scenario | Strategie | Reden | |----------|-----------|-------| | HTTP API, voornamelijk I/O | Cluster-module (1 worker per core) | Maximaliseert verbindingsdoorvoer | | Beeld-/videoverwerking | Worker thread pool (4-8 threads) | Houdt de event loop responsief | | CPU-zware datapipeline | Worker threads + SharedArrayBuffer | Zero-copy gegevensuitwisseling | | Microservices op schaal | Kubernetes-pods (single-process containers) | Orchestrator beheert de schaling | | Gemengd I/O + CPU | Cluster + worker pool per worker | Elke worker offload CPU naar threads | Voor gecontaineriseerde deployments op Kubernetes is het draaien van een enkel Node.js-proces per container (zonder clustering) vaak eenvoudiger en voorspelbaarder. De orchestrator handelt horizontale schaling, health checks en rolling restarts af. Clustering voegt waarde toe op bare metal of VM's waar een enkele machine het coregebruik moet maximaliseren. > **Prestatieverbeteringen in Node.js 24** > > Node.js 24, uitgebracht in 2025, bevat V8 12.4 met 8-12% hogere doorvoer bij API-workloads. De ingebouwde `fetch()` gebruikt nu Undici 7.0 met standaard HTTP/2- en HTTP/3-ondersteuning. Het permissiemodel maakt het bovendien mogelijk om bestandssysteem- en netwerktoegang per worker te beperken voor defense-in-depth. ## Praktische optimalisatiechecklist voor productie Het toepassen van deze technieken in volgorde van impact — eerst event loop-gezondheid, dan schaling, dan fijnafstemming — levert meetbare verbeteringen op met minimaal risico. - **Event loop-gebruik continu monitoren** met `performance.eventLoopUtilization()` — alert bij 70% drempel - **Profileren vóór optimaliseren** — gebruik `node --prof` of OpenTelemetry-traces om daadwerkelijke knelpunten te identificeren, niet veronderstelde - **Synchrone operaties van de hoofdthread verplaatsen** — `crypto.pbkdf2`, `JSON.parse` op grote payloads en beeldverwerking horen thuis in worker threads - **`UV_THREADPOOL_SIZE` instellen op basis van workload** — standaard 4 is te laag voor apps met intensieve DNS- of bestands-I/O, te hoog in geclusterde setups - **Streaming gebruiken voor grote datasets** — `JSONStream`, `csv-parser` en Node.js `stream.pipeline()` voorkomen geheugenpieken bij grote payloads - **Worker pools opwarmen bij het opstarten** — vermijd cold-start-latentie bij eerste verzoeken naar CPU-intensieve endpoints - **V8 heap-vlaggen afstemmen op de deployment** — `--max-old-space-size` en `--max-semi-space-size` op basis van beschikbaar geheugen en allocatiepatronen - **Graceful shutdown implementeren** — verbindingen draineren bij SIGTERM, databasepools sluiten en lopende verzoeken laten voltooien voor het afsluiten van het proces ## Conclusie De performance van Node.js in 2026 draait om drie fundamentele principes: - De event loop is het single point of failure voor doorvoer — het monitoren van het gebruik en het elimineren van blokkers heeft de grootste impact op latentie - Clustering schaalt I/O-gebonden workloads over cores; worker threads offloaden CPU-gebonden taken van de event loop — het gecombineerd gebruik van beide zorgt voor maximale hardwarebenutting - Productiemonitoring met [OpenTelemetry](https://opentelemetry.io/docs/languages/js/) en V8-profilingtools verandert giswerk in data — stel alerts in op event loop lag p99 en heap-groeisnelheid - Node.js 24 brengt V8 12.4-prestatiewinst, stabiel HTTP/3 via Undici 7 en een volwassen permissiemodel — upgraden vanaf Node.js 20 of eerder levert meetbare doorvoerverbeteringen op - Begin met `performance.eventLoopUtilization()` bij elke bestaande Node.js-applicatie — de resultaten onthullen vaak de enige optimalisatie met de grootste impact --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/node-nestjs/nodejs-performance-event-loop-clustering-optimization