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.
📋 Table of Contents
- Erstens: Um welche Situation handelt es sich?
- Schnelle Lösung: Erhöhen Sie das Heap-Limit
- Ursache 1: Ganze Dateien in den Speicher einlesen
- Ursache 2: Unbegrenzte Abfrageergebnisse
- Ursache 3: Echte Lecks in Servern mit langer Laufzeit
- Mit Heap-Snapshots ein Leck finden
- Ursache 4: Builds haben nicht mehr genügend Speicher
- Docker- und Container-Limits
- Diagnosesequenz
- Häufig gestellte Fragen
- Fazit
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
- 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.
- Protokoll
process.memoryUsage()im Laufe der Zeit. Ein monotoner Anstieg bedeutet ein Leck. - Wenn dies nur bei großen Eingaben fehlschlägt, suchen Sie nach dem Laden der gesamten Datei oder des gesamten Ergebnissatzes.
- Wenn es sich um einen Server mit langer Laufzeit handelt, erstellen Sie zwei Heap-Snapshots und vergleichen Sie die Deltas.
- Überprüfen Sie den Haltebaum auf wachsende Objekte, um herauszufinden, was sie festhält.
- 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.
🔗 Share this article
✍️ Leave a Comment