Memory leaks opsporen in Node.js: praktische gids

Leer hoe je memory leaks in Node.js opspoort en oplost. Van heap snapshots en profiling tot veelvoorkomende oorzaken en praktische tools.

24 juli 20268 min leestijdDoor We Develop Communication

Wanneer je Node.js applicatie na dagen of weken draaien ineens crasht met een JavaScript heap out of memory error, heb je waarschijnlijk te maken met memory leaks. Het opsporen van memory leaks in Node.js is een van de lastigere performance-uitdagingen, omdat ze zich pas manifesteren onder productiedruk en vaak subtiel zijn.

In deze gids duiken we in hoe memory management in Node.js werkt, welke tools je inzet om leaks op te sporen, en welke patronen je moet vermijden. Of je nu een long-running API draait of een worker process, deze kennis bespaart je middagen aan frustratie.

Hoe geheugenbeheer in Node.js werkt

Node.js draait op de V8-engine, die gebruikmaakt van een generational garbage collector. Objecten die kort leven komen in de young generation (new space), en objecten die meerdere GC-cycli overleven promoveren naar de old generation (old space).

De garbage collector ruimt automatisch objecten op die geen references meer hebben. Een memory leak ontstaat wanneer je per ongeluk references blijft vasthouden naar objecten die je niet meer nodig hebt, waardoor de GC ze niet kan opruimen.

Er zijn drie belangrijke geheugencategorieën:

  • Heap used: het geheugen dat V8 actief gebruikt voor JavaScript objecten
  • Heap total: de totale hoeveelheid die V8 heeft gereserveerd
  • RSS (Resident Set Size): al het geheugen dat het proces gebruikt, inclusief native allocations

Voor een dieper begrip van hoe Node.js zelf werkt, lees onze uitleg over wat Node.js is en hoe de event loop werkt.

Signalen dat je een memory leak hebt

De eerste stap is herkennen dát je een leak hebt. Let op deze signalen:

  • Heap used stijgt gestaag over uren of dagen, zelfs bij constante load
  • RSS groeit zonder dat het plafond wordt bereikt
  • Response times worden langzamer naarmate het proces langer draait
  • Processes crashen periodiek met out-of-memory errors
  • CPU-gebruik stijgt door steeds langere GC-cycli

Een snelle sanity check doe je met process.memoryUsage():

setInterval(() => {
  const mem = process.memoryUsage();
  console.log({
    rss: `${Math.round(mem.rss / 1024 / 1024)} MB`,
    heapUsed: `${Math.round(mem.heapUsed / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(mem.heapTotal / 1024 / 1024)} MB`,
    external: `${Math.round(mem.external / 1024 / 1024)} MB`,
  });
}, 30000);

In productie exporteer je deze waarden naar een monitoring-tool zoals Prometheus, Datadog of New Relic. Zo zie je trends over dagen in plaats van minuten.

Veelvoorkomende oorzaken van memory leaks

Voordat je uren spendeert aan profiling, loop deze klassiekers langs. Ze zijn verantwoordelijk voor het overgrote deel van de leaks die we in de praktijk tegenkomen.

1. Unbounded caches

Een in-memory cache zonder size limit is een tikkende tijdbom:

const cache = {};

function getUser(id) {
  if (!cache[id]) {
    cache[id] = fetchUserFromDb(id);
  }
  return cache[id];
}

Gebruik in plaats daarvan een LRU-cache met een harde limit, zoals lru-cache:

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({
  max: 1000,
  ttl: 1000 * 60 * 5,
});

2. Event listeners die blijven hangen

Elke .on() zonder corresponderende .off() of removeListener() houdt references vast. In lang draaiende processes met veel events stapelt dit snel op.

function handleRequest(req) {
  req.on('data', processChunk);
  req.on('end', () => {
    req.removeListener('data', processChunk);
  });
}

Moderne code gebruikt een AbortController om listeners netjes op te ruimen. Ook helpt het om EventEmitter.setMaxListeners() op een redelijke waarde te zetten, krijg je een MaxListenersExceededWarning, dan is dat vrijwel altijd een leak.

3. Timers en intervals

Een setInterval die nooit wordt geclearscheduled kan closures levend houden:

function startPolling(user) {
  setInterval(() => {
    checkStatus(user);
  }, 5000);
}

De user reference blijft in de interval callback hangen, voor altijd. Bewaar de interval ID en ruim hem op wanneer je klaar bent.

4. Closures die grote objecten vasthouden

Closures zijn krachtig, maar ze vangen soms meer op dan je denkt:

function processLargeData(data) {
  return () => data.id;
}

De returned functie houdt de hele data referentie vast, ook al gebruik je alleen id. Extraheer wat je nodig hebt voordat je de closure maakt.

5. Global state in modules

Module-level variabelen leven zo lang het proces leeft. Een array die je in een module bijhoudt en waar je alleen aan toevoegt, is effectief een leak. Zie ook onze gids over modules en projectstructuur in Node.js.

Heap snapshots maken en analyseren

De krachtigste techniek voor memory leaks opsporen is de three-snapshot methode. Je maakt drie heap snapshots, een baseline, een na wat werk, en een na nog meer werk, en vergelijkt ze.

Snapshots maken via Chrome DevTools

Start je Node.js proces met de inspector:

node --inspect server.js

Open chrome://inspect in Chrome, klik op "inspect" bij je proces, en ga naar de Memory-tab. Kies "Heap snapshot" en klik op "Take snapshot".

Laat je applicatie wat werk doen (bijvoorbeeld een paar honderd requests), neem een tweede snapshot, doe nog meer werk, en neem een derde. Vergelijk dan snapshot 2 en 3 met "Objects allocated between snapshot 1 and 2", objecten die ook na snapshot 3 nog leven zijn verdachten.

Programmatisch snapshots maken

In productie wil je soms een snapshot maken zonder DevTools:

import { writeHeapSnapshot } from 'v8';

process.on('SIGUSR2', () => {
  const filename = `heap-${Date.now()}.heapsnapshot`;
  writeHeapSnapshot(filename);
  console.log(`Heap snapshot written to ${filename}`);
});

Stuur kill -SIGUSR2 <pid> om een snapshot te triggeren. Download het bestand en open het in Chrome DevTools voor analyse.

Professionele tooling

Voor serieuze profiling zijn er specifieke tools die verder gaan dan DevTools:

  • clinic.js, een toolsuite van NearForm met Doctor, Bubbleprof en Heapprofiler. Draai clinic doctor -- node server.js voor een automatische diagnose.
  • 0x, flame graph generator voor CPU en memory profiling.
  • heapdump, legacy package maar nog steeds bruikbaar voor heap dumps in oudere Node.js versies.
  • node --heap-prof, ingebouwde heap profiler sinds Node.js 12.

De officiële Node.js documentatie over diagnostics geeft een goede introductie tot de ingebouwde tools.

Een praktisch debugging-proces

Loop dit stappenplan af wanneer je een leak vermoedt:

  1. Reproduceer lokaal: genereer load met een tool als autocannon of k6 en monitor het geheugen
  2. Maak een baseline: noteer heap used na een warm-up periode
  3. Genereer constante load: laat het systeem enkele minuten draaien
  4. Vergelijk metingen: is het geheugen significant gegroeid?
  5. Maak snapshots: gebruik de three-snapshot methode om retained objecten te identificeren
  6. Analyseer retainers: kijk in DevTools welke references een object levend houden
  7. Fix en verifieer: herhaal de meting na je fix

Deze aanpak combineert goed met de bredere strategieën uit onze performance tuning gids voor Node.js.

Memory leaks in frameworks en libraries

Soms ligt de leak niet in je eigen code. Veelvoorkomende bronnen:

  • Express middleware: per-request state die per ongeluk aan de app wordt toegevoegd. Zie ook onze gids over middleware patterns.
  • Database pools: connections die niet worden vrijgegeven blokkeren de pool en houden query results vast.
  • Redis clients: subscribers die niet worden afgemeld houden verbindingen open.
  • HTTP agents: oude versies van keep-alive agents hadden bekende leaks.

Check altijd de changelogs en GitHub issues van je dependencies. Een npm outdated gevolgd door gerichte updates lost meer leaks op dan je zou denken.

Worker threads en child processes

Bij clustering en scaling en background jobs heb je meerdere processen of threads. Een leak in één worker betekent niet automatisch een leak in je main process.

Profileer workers apart. Gebruik bijvoorbeeld process.pid in je logging zodat je weet welk proces je bekijkt. In worker threads kun je met resourceLimits een hard plafond zetten op geheugengebruik:

new Worker('./worker.js', {
  resourceLimits: {
    maxOldGenerationSizeMb: 512,
    maxYoungGenerationSizeMb: 64,
  }
});

Zo crasht de worker in plaats van je hele applicatie mee te slepen.

Preventie is beter dan genezen

Een paar patronen die leaks voorkomen:

  • Gebruik WeakMap en WeakRef voor associaties die de GC niet mogen blokkeren
  • Zet altijd een bovengrens op caches, queues en buffers
  • Gebruik AbortController voor listeners, fetches en timers
  • Test long-running scenario's in je CI, niet alleen snelle unit tests
  • Monitor heap usage in productie met alerts op groeiratio's, niet alleen absolute waardes

Voor TypeScript projecten geeft TypeScript je type checking die sommige leak-patronen zichtbaar maakt, zoals het niet consumeren van een stream.

Veelgestelde vragen

Wat is een memory leak in Node.js? Een memory leak is geheugen dat je applicatie blijft vasthouden terwijl het niet meer nodig is. In Node.js uit zich dat vaak in een langzaam groeiende RSS of heap size, totdat de V8-engine crasht met een out-of-memory error.

Hoe zie ik of mijn Node.js applicatie een memory leak heeft? Monitor de heap used en RSS over tijd. Als deze waarden blijven stijgen zonder terug te zakken na een garbage collection, heb je waarschijnlijk een leak. Tools zoals process.memoryUsage(), Prometheus of New Relic helpen bij het zichtbaar maken.

Wat zijn veelvoorkomende oorzaken van memory leaks in Node.js? Vaak zijn het globale caches zonder eviction, event listeners die niet worden opgeruimd, closures die onbedoeld grote objecten vasthouden, en timers die blijven draaien. Ook niet-opgeruimde database connecties of streams zijn klassieke boosdoeners.

Welke tools gebruik ik om heap snapshots te analyseren? Chrome DevTools is de meest gebruikte tool. Je kunt heap snapshots maken via de --inspect flag of programmatisch met het v8 module. Voor productie zijn clinic.js, 0x en heapdump populaire keuzes.

Hoe voorkom ik memory leaks in productie? Gebruik bounded caches, ruim event listeners op met removeListener of AbortController, beperk het aantal luisteraars, en monitor actief je heap usage. Een goede strategie voor error handling en graceful shutdowns helpt ook.

Tot slot

Memory leaks opsporen in Node.js vraagt geduld en systematisch werk. Begin bij het herkennen van de symptomen, loop de veelvoorkomende oorzaken langs, en pak pas daarna de heap snapshots erbij. Met goede monitoring en de juiste tooling vind je vrijwel elke leak binnen een uur of twee.

De belangrijkste les: bouw vanaf het begin met bounded resources en ruim altijd op wat je creëert. Dan blijven memory leaks een uitzondering in plaats van een terugkerende nachtmerrie.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.