[object Object]
Discord.js Memory Leaks beheben: V8 Heap-Analyse & Cache-Strategien
Discord-Bots, die auf großen Servern betrieben werden oder über Wochen ohne Neustart laufen, leiden häufig unter schleichendem RAM-Wachstum. Ein Bot, der nach dem Start 120 MB Arbeitsspeicher belegt, kann innerhalb weniger Tage mehrere Gigabyte verbrauchen, bis die Node.js-Runtime den Prozess mit einem FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory beendet.
In diesem Leitfaden analysieren wir die tatsächlichen Ursachen von Speicherlecks in discord.js v14, demonstrieren die V8 Heap-Snapshot-Analyse und implementieren produktionsreife Cache-Sweeping-Strategien.
1. Die Hauptursachen für Memory Leaks in Discord-Bots
In JavaScript verhindert der automatische Garbage Collector (GC), dass nicht mehr referenzierter Speicher weiter belegt bleibt. Ein Memory Leak entsteht dann, wenn Objekte im Heap verbleiben, weil unbeabsichtigte Referenzen aus dem Root-Scope (z. B. globale Maps, Closures oder ungelöschte Event-Listener) auf sie zeigen.
A. Der unbegrenzte Standard-Cache von Discord.js
In der Standardkonfiguration legt discord.js fast jedes empfangene Event im internen Cache ab:
- Jede empfangene Nachricht (
client.channels.cache.get(id).messages) - Jeder Server-Benutzer (
guild.members.cache) - Jeder Voice-State und jede Präsenz-Aktualisierung
Auf Servern mit 50.000 Mitgliedern führt dies schnell dazu, dass Hunderttausende User- und Message-Objekte im RAM gehalten werden, selbst wenn sie nie wieder benötigt werden.
B. Akkumulierte Event-Listener in Modulen
Ein klassischer Fehler in modularen Bots: Bei jedem Reload eines Befehls oder Moduls wird client.on(...) erneut aufgerufen, ohne dass der alte Listener via client.off(...) deregistriert wurde:
// GEFÄHRLICH: Erzeugt bei jedem Modul-Reload einen neuen Listener im Heap
export function initTicketWatcher(client: Client) {
client.on('messageCreate', (message) => {
handleTicketMessage(message);
});
}
2. V8 Heap-Snapshots zur Ursachenanalyse erstellen
Um herauszufinden, welche Objekte den Heap blockieren, erstellen wir programmatisch Heap-Dumps mit dem nativen v8-Modul von Node.js:
import v8 from 'node:v8';
import fs from 'node:fs';
import path from 'node:path';
export function takeHeapSnapshot(label: string = 'leak-audit'): string {
const memUsage = process.memoryUsage();
console.log(`[Heap Profiler] RSS: ${(memUsage.rss / 1024 / 1024).toFixed(2)} MB, Heap: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`);
const filename = path.join(process.cwd(), `heap-${label}-${Date.now()}.heapsnapshot`);
const snapshotStream = v8.getHeapSnapshot();
const fileStream = fs.createWriteStream(filename);
snapshotStream.pipe(fileStream);
fileStream.on('finish', () => {
console.log(`[Heap Profiler] Snapshot erfolgreich gespeichert: ${filename}`);
});
return filename;
}
Auswertung in Chrome DevTools:
- Öffne im Chrome-Browser
chrome://inspect. - Klicke auf "Open dedicated DevTools for Node" oder gehe in den Tab Memory.
- Klicke auf Load und wähle die generierte
.heapsnapshot-Datei aus. - Sortiere nach Retained Size (Speichergröße inklusive aller abhängigen Referenzen).
- Halte Ausschau nach Instanzen von
Message,GuildMemberoderClosure.
3. Best Practice: Benutzerdefinierte Cache-Manager & Sweeper
In modernen Produktionsumgebungen begrenzen wir die Caches von discord.js rigoros über die Client-Optionen:
import { Client, GatewayIntentBits, Options } from 'discord.js';
export const botClient = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
makeCache: Options.cacheWithLimits({
// Nachrichten nur bis zu 25 Stück pro Kanal cachen
MessageManager: 25,
// Präsenzen und Aktivitäten komplett deaktivieren
PresenceManager: 0,
// Reaktionen nicht im Speicher vorhalten
ReactionManager: 0,
// User-Profile auf maximale Lebensdauer begrenzen
UserManager: {
maxSize: 500,
keepOverLimit: (value) => value.id === client.user?.id,
},
// Voice-States nur für aktive Kanäle
VoiceStateManager: 100,
}),
sweepers: {
// Automatisches Bereinigen alle 30 Minuten
...Options.defaultSweeperSettings,
messages: {
interval: 1800, // 30 Minuten
lifetime: 900, // Nachrichten älter als 15 Minuten entfernen
},
users: {
interval: 3600, // 1 Stunde
filter: () => (user) => !user.bot, // Nur inaktive Nicht-Bots verwerfen
},
},
});
4. Schwache Referenzen mit WeakMap und WeakSet
Wenn dein Bot temporäre Metadaten zu Nachrichten oder Interaktionen speichern muss, nutze WeakMap anstelle von regulären Map-Objekten:
// RICHTIG: Der Garbage Collector kann Message-Objekte abräumen,
// sobald sie von discord.js verworfen werden!
export const userCooldowns = new WeakMap<Message, number>();
export function registerCooldown(message: Message, durationMs: number) {
userCooldowns.set(message, Date.now() + durationMs);
}
Mit WeakMap verhindert der Key nicht, dass das Originalobjekt freigegeben wird. Sobald der Message-Cache geleert wird, verschwindet der Eintrag automatisch aus dem Speicher.
5. Zusammenfassung & Checkliste für 24/7 Uptime
| Maßnahme | Empfohlener Wert | Wirkung |
|---|---|---|
MessageManager Cache-Limit | 10–50 pro Kanal | Verhindert lineares RAM-Wachstum bei Chat-Aktivität |
| Message Sweeper Intervall | 1800 s (30 Min) | Entfernt alte Nachrichten zuverlässig aus dem Heap |
| Presence Cache | 0 (Deaktiviert) | Spart bis zu 70 % Speicher auf großen Community-Servern |
| Event-Listener Deregistrierung | Immer client.off() bei Modul-Reload | Verhindert Closure-Leaks im Root-Scope |
| V8 Heap Limit | --max-old-space-size=512 | Zwingt Node.js zu früherer Garbage Collection |
Luca Lichtenwald
Spezialisiert auf hochverfügbare Discord-Bot-Infrastrukturen, V8-Memory-Tuning und kryptografische Zero-Trust-Architekturen.