docs//[object Object]

[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:

  1. Öffne im Chrome-Browser chrome://inspect.
  2. Klicke auf "Open dedicated DevTools for Node" oder gehe in den Tab Memory.
  3. Klicke auf Load und wähle die generierte .heapsnapshot-Datei aus.
  4. Sortiere nach Retained Size (Speichergröße inklusive aller abhängigen Referenzen).
  5. Halte Ausschau nach Instanzen von Message, GuildMember oder Closure.

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ßnahmeEmpfohlener WertWirkung
MessageManager Cache-Limit10–50 pro KanalVerhindert lineares RAM-Wachstum bei Chat-Aktivität
Message Sweeper Intervall1800 s (30 Min)Entfernt alte Nachrichten zuverlässig aus dem Heap
Presence Cache0 (Deaktiviert)Spart bis zu 70 % Speicher auf großen Community-Servern
Event-Listener DeregistrierungImmer client.off() bei Modul-ReloadVerhindert Closure-Leaks im Root-Scope
V8 Heap Limit--max-old-space-size=512Zwingt Node.js zu früherer Garbage Collection
Luca Lichtenwald
Artikel verfasst vonGründer & Lead Cloud Architect

Luca Lichtenwald

Spezialisiert auf hochverfügbare Discord-Bot-Infrastrukturen, V8-Memory-Tuning und kryptografische Zero-Trust-Architekturen.

Cookie-Einstellungen

Wir verwenden Cookies, um deine Erfahrung zu verbessern und unsere Dienste optimal anzubieten. Du kannst deine Einstellungen jederzeit anpassen.

main/11f725c