🌐 Detecting your location…

So beheben Sie den Fehler „JavaScript-Heap nicht genügend Speicher“ in Node.js

⏱️7 min read  ·  1,529 words

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memorybedeutet, dass der V8 seine Höchstgrenze erreicht hat. Dahinter stecken zwei sehr unterschiedliche Situationen: eine legitime Arbeitslast, die mehr Speicher benötigt, und ein Leck oder ein Algorithmus, der weit mehr lädt, als er sollte. Durch die Erhöhung des Limits wird das erste behoben und das zweite ausgeblendet.

Erstens: Um welche Situation handelt es sich?

Beantworten Sie diese Frage, bevor Sie etwas ändern.

Symptom Wahrscheinliche Ursache
Schlägt während eines Builds oder eines großen Batch-Jobs fehl Benötigt zu Recht mehr Heap
Server stürzt nach Stunden oder Tagen Betriebszeit ab Speicherverlust
Schlägt bei großen Eingaben fehl, bei kleinen in Ordnung Alles in den Speicher laden statt streamen
Stürzt bei jeder Größe sofort ab Unbegrenzte Rekursion oder eine unendliche Akkumulation

Ein Server mit langer Laufzeit, der stetig wächst, weist ein Leck auf. Eine Erhöhung des Limits verschiebt den Absturz nur.

Schnelle Lösung: Erhöhen Sie das Heap-Limit

Geeignet für Builds und Batch-Jobs, die den Speicher wirklich benötigen.

# Per invocation, in megabytes
node --max-old-space-size=4096 script.js

# For anything Node spawns, including build tools
export NODE_OPTIONS="--max-old-space-size=4096"
npm run build
{
  "scripts": {
    "build": "NODE_OPTIONS=--max-old-space-size=4096 next build"
  }
}

Stellen Sie dies nicht über den tatsächlich verfügbaren Speicher hinaus ein. Wenn der Container 2 GB hat und Sie einen 4 GB-Heap zulassen, beendet der OOM-Killer des Kernels den Prozess mit SIGKILL – und Sie erhalten überhaupt keinen JavaScript-Fehler, sondern nur den Exit-Code 137, der viel schwieriger zu diagnostizieren ist.

# Exit code 137 = 128 + 9 (SIGKILL) — killed by the OS, not by V8
docker inspect <container> --format='{{.State.ExitCode}}'
dmesg | grep -i "killed process"

Ursache 1: Ganze Dateien in den Speicher einlesen

Die häufigste Ursache im Datenverarbeitungscode.

// ❌ A 2GB file needs 2GB+ of heap, plus overhead for the string
import fs from 'node:fs/promises';
const content = await fs.readFile('huge.csv', 'utf8');
const lines = content.split('\n');       // now a second copy exists
// ✅ Constant memory regardless of file size
import fs from 'node:fs';
import readline from 'node:readline';

const stream = fs.createReadStream('huge.csv', { encoding: 'utf8' });
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });

for await (const line of rl) {
  await processLine(line);
}

Gleiches gilt für HTTP-Antworten und Datenbankergebnisse. Streamen Sie sie; akkumulieren Sie sie nicht.

// ❌ Buffers the entire response
const res = await fetch(url);
const buffer = await res.arrayBuffer();

// ✅ Pipes straight to disk
import { pipeline } from 'node:stream/promises';
import { Writable } from 'node:stream';

await pipeline(res.body, fs.createWriteStream('output.bin'));

Ursache 2: Unbegrenzte Abfrageergebnisse

// ❌ Ten million rows, all resident at once
const users = await db.query('SELECT * FROM users');
for (const user of users) await sendEmail(user);
// ✅ Keyset-paginated batches — constant memory
let lastId = 0;
const BATCH = 1000;

while (true) {
  const rows = await db.query(
    'SELECT * FROM users WHERE id > $1 ORDER BY id LIMIT $2',
    [lastId, BATCH]
  );
  if (rows.length === 0) break;

  for (const user of rows) await sendEmail(user);
  lastId = rows[rows.length - 1].id;
}

Besser noch: Verwenden Sie einen Datenbankcursor, wenn Ihr Treiber einen unterstützt – er streamt Zeilen, ohne den vollständigen Ergebnissatz auf beiden Seiten zu halten.

Ursache 3: Echte Lecks in Servern mit langer Laufzeit

Die meisten davon sind auf vier Muster zurückzuführen.

Ein unbegrenzter Cache. Ein einfaches Objekt oder eine Karte, die immer nur wächst.

// ❌ Grows forever
const cache = new Map();
function get(key) {
  if (!cache.has(key)) cache.set(key, expensive(key));
  return cache.get(key);
}
// ✅ Bounded with eviction
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({ max: 5000, ttl: 1000 * 60 * 10 });

Zuhörer, die niemals entfernt werden.

// ❌ Adds a listener on every request
app.get('/data', (req, res) => {
  emitter.on('update', () => res.write('...'));
});

// ✅ Remove it when the request ends
app.get('/data', (req, res) => {
  const onUpdate = () => res.write('...');
  emitter.on('update', onUpdate);
  res.on('close', () => emitter.off('update', onUpdate));
});

Node warnt davor –MaxListenersExceededWarning ist ein Leckanzeiger, kein Geräusch zur Unterdrückung.

Timer, die nie gelöscht werden. JedessetInterval hält seinen Abschluss und alles, worauf er sich bezieht, für immer am Leben.

Verschlüsse zum Auffangen großer Objekte. Ein Rückruf, der auf ein Feld eines großen Objekts verweist, sorgt dafür, dass das gesamte Objekt erreichbar ist.

Mit Heap-Snapshots ein Leck finden

Raten ist langsam. Machen Sie Schnappschüsse und vergleichen Sie.

# Start with the inspector attached
node --inspect server.js
# Open chrome://inspect in Chrome, click "inspect", go to the Memory tab

Das Verfahren: Erstellen Sie einen Snapshot, führen Sie den verdächtigen Workload eine Weile aus, erstellen Sie einen zweiten Snapshot und sortieren Sie dann in der Vergleichsansicht nach „Delta“. Objekte, die zwischen den Schnappschüssen wachsen und niemals schrumpfen, sind Ihr Leck, und der Retainer-Baum zeigt genau, was sie am Leben erhält.

Sie können Snapshots auch aus dem Prozess heraus auslösen, was in der Produktion nützlich ist.

import v8 from 'node:v8';
import fs from 'node:fs';

process.on('SIGUSR2', () => {
  const file = `/tmp/heap-${Date.now()}.heapsnapshot`;
  fs.writeFileSync(file, v8.getHeapSnapshot());
  console.log('Heap snapshot written to', file);
});
// Then: kill -SIGUSR2 <pid>

Protokollieren Sie die Heap-Nutzung kontinuierlich, um ein Leck zu bestätigen, bevor Sie danach suchen.

setInterval(() => {
  const m = process.memoryUsage();
  console.log({
    rss:       `${(m.rss / 1e6).toFixed(0)}MB`,
    heapUsed:  `${(m.heapUsed / 1e6).toFixed(0)}MB`,
    heapTotal: `${(m.heapTotal / 1e6).toFixed(0)}MB`,
    external:  `${(m.external / 1e6).toFixed(0)}MB`,
  });
}, 30_000);

Ein Sägezahnmuster ist gesund – Garbage Collection gewinnt Speicher zurück. Eine Treppe, die nur nach oben führt, ist ein Leck.

Ursache 4: Builds haben nicht mehr genügend Speicher

Große TypeScript-, Webpack- oder Next.js-Builds sind speicherhungrig und schlagen häufig in CI-Containern mit bescheidenen Einschränkungen fehl.

# Give the build more headroom
NODE_OPTIONS=--max-old-space-size=6144 npm run build

# TypeScript: incremental builds reuse previous work
tsc --incremental --noEmit

# Split very large builds into projects
tsc --build tsconfig.json

Stellen Sie in CI sicher, dass der Läufer tatsächlich über den Speicher verfügt, den Sie zulassen. Eine 6-GB-Heap-Begrenzung auf einem 4-GB-Runner führt eher zu einem OOM-Kill als zu einem hilfreichen Fehler.

Docker- und Container-Limits

Der Knoten passt die Größe seines Heaps nicht in jeder Konfiguration automatisch an ein Containerlimit an. Legen Sie daher beides explizit fest.

services:
  api:
    image: my-api
    environment:
      # Keep the heap below the container limit, leaving room for
      # native allocations, buffers, and the runtime itself.
      NODE_OPTIONS: "--max-old-space-size=1536"
    deploy:
      resources:
        limits:
          memory: 2G

Die Lücke zwischen dem Heap-Limit und dem Container-Limit ist wichtig. Puffer, native Module und die Laufzeit werden alle außerhalb des JavaScript-Heaps zugewiesen und--max-old-space-size berücksichtigt sie nicht.

Diagnosesequenz

  1. Stellen Sie fest, ob es sich um einen V8-Fehler oder einen Betriebssystem-Kill handelt – Exit-Code 137 bedeutet, dass der Kernel den Fehler verursacht hat.
  2. Protokollprocess.memoryUsage() im Laufe der Zeit. Ein monotoner Anstieg bedeutet ein Leck.
  3. Wenn dies nur bei großen Eingaben fehlschlägt, suchen Sie nach dem Laden der gesamten Datei oder des gesamten Ergebnissatzes.
  4. Wenn es sich um einen Server mit langer Laufzeit handelt, erstellen Sie zwei Heap-Snapshots und vergleichen Sie die Deltas.
  5. Überprüfen Sie den Haltebaum auf wachsende Objekte, um herauszufinden, was sie festhält.
  6. Erhöhen Sie das Heap-Limit erst, wenn Sie festgestellt haben, dass die Nutzung legitim ist.

Häufig gestellte Fragen

F: Wie hoch ist das Standard-Heap-Limit?
A: Es hängt von der Node-Version und dem verfügbaren Systemspeicher ab und moderne Versionen passen die Größe sinnvoller an als ältere. Überprüfen Sie Ihre mitnode -e "console.log(v8.getHeapStatistics().heap_size_limit / 1e6)".

F: Ist die Erhöhung der maximalen Größe des alten Speicherplatzes sicher?
A: Für Builds und Batch-Jobs mit echtem Speicherbedarf, ja – vorausgesetzt, die Maschine verfügt über den Speicher. Bei einem leckenden Server verschiebt es den Absturz und erschwert die Diagnose.

F: Warum stürzt meine App mit dem Exit-Code 137 und ohne Fehler ab?
A: Der OOM-Killer des Kernels hat SIGKILL gesendet, das nicht abgefangen werden kann. Ihr Heap-Limit überschreitet das Container-Limit oder etwas außerhalb des Heaps verbraucht Speicher.

F: Kann ich die Speicherbereinigung erzwingen?
A: Mit--expose-gc Sie könnenglobal.gc()aufrufen , was nützlich ist, um zu testen, ob der Speicher wirklich erhalten bleibt. Es handelt sich nicht um eine Lösung – wenn der Speicher nicht zurückgefordert wird, verweist immer noch etwas darauf.

F: Helfen Worker-Threads?
A: Jeder Worker erhält seinen eigenen Heap, sodass die Aufteilung der Arbeit auf alle Worker die Gesamtkapazität erhöht. Ein Leck wird dadurch nicht behoben; es verteilt es.

Fazit

Beginnen Sie mit der Klassifizierung des Fehlers:Ein V8-Heap-Fehler bei einem Build bedeutet normalerweise, dass die Arbeit tatsächlich mehr Speicher benötigt, während ein stetiges Wachstum bei einem Server mit langer Laufzeit ein Leck bedeutet. Streamen Sie Dateien und paginieren Sie Abfrageergebnisse, anstatt alles auf einmal zu laden, binden Sie jeden Cache mit Größen- und TTL-Grenzwerten, entfernen Sie Listener und löschen Sie Timer, wenn ihr Gültigkeitsbereich endet, und verwenden Sie den Heap-Snapshot-Vergleich, um herauszufinden, was tatsächlich aufbewahrt wird. Erhöhe--max-old-space-size erst nach der Bestätigung, dass die Nutzung legitim ist, und halten Sie sie immer bequem unter der Speichergrenze des Containers.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work — which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা