ProgrammierungTechnologie

Zuverlässige Hintergrund-Jobs mit BullMQ und Redis für viele Anwendungen

netcomputing.de Redaktion26. August 202612 Min. Lesezeit

Zusammenfassung

Ein HTTP-Request, der eine E-Mail verschickt oder eine langsame externe API aufruft, lässt Nutzer warten und verliert die Arbeit bei einem Serverneustart. BullMQ, eine quelloffene Job-Queue-Bibliothek für Node.js auf Redis-Basis, legt den Job sofort in einer Warteschlange ab, ein separater Worker verarbeitet ihn im Hintergrund und wiederholt einen Fehlschlag automatisch mit steigender Wartezeit. Dieselbe Redis-Instanz, bereits Cache- und Sessionschicht, übernimmt zusätzlich die dauerhafte Job-Warteschlange mehrerer Anwendungen.

Ein Node.js-Request-Handler, der eine Bestätigungs-E-Mail verschickt, ein PDF-Dokument erzeugt, ein hochgeladenes Bild verkleinert oder eine langsame externe API aufruft, blockiert die HTTP-Antwort so lange, wie diese Arbeit dauert. Der Nutzer wartet auf eine Antwort, die mit seiner eigentlichen Anfrage kaum noch etwas zu tun hat, und ein Neustart des Servers mitten in dieser Arbeit wirft sie ersatzlos weg: Die E-Mail wird nie verschickt, das PDF nie erzeugt, der externe Aufruf nie wiederholt. Je mehr Anwendungen in einem Portfolio auf dieselbe Weise arbeiten, desto häufiger passiert genau das.

In einer Anwendungslandschaft, die eine Redis-Instanz bereits als gemeinsame Cache- und Sessionschicht für mehrere Anwendungen betreibt, braucht es für Hintergrund-Jobs keine zweite Infrastrukturkomponente. Dieselbe Redis-Instanz übernimmt zusätzlich die Rolle des dauerhaften Speichers für BullMQ, eine quelloffene Job-Queue-Bibliothek für Node.js, die Aufgaben aus einem HTTP-Request herauslöst und in einem separaten Worker-Prozess abarbeitet. Die Anfrage kehrt sofort zurück, sobald der Job in der Warteschlange liegt, und ein fehlgeschlagener Job wird automatisch mit steigender Wartezeit erneut versucht, statt einfach zu verschwinden. Anders als bei Cache-Werten oder Sitzungsdaten muss die Warteschlange selbst einen Neustart überstehen, und genau das stellt an dieselbe Redis-Instanz eine neue, strengere Anforderung, die weiter unten in den Fehlerquellen konkret wird.

Systemvoraussetzungen

Die folgende Tabelle listet die aktuellen Versionen und Anforderungen für einen Hintergrund-Job-Aufbau mit BullMQ auf einer bereits vorhandenen, gemeinsam genutzten Redis-Instanz, Stand der BullMQ-eigenen Dokumentation und des offiziellen GitHub-Repositorys.

KomponenteVersion oder AnforderungZweck
BullMQVersion 6.3.0 (26. August 2026, aktuelle Hauptversion 6), laut dem offiziellen GitHub-Repository fortlaufend in Patch-Releases weiterentwickeltJob-Queue-Bibliothek für Node.js auf Redis-Basis
RedisDieselbe gemeinsam genutzte Instanz wie für Cache und Sitzungen, mit auf noeviction umgestelltem maxmemory-policyDauerhafter Speicher für Warteschlangen, Jobs und ihren Status
Node.jsVersion 14.17.0 oder neuer laut dem engines-Feld des npm-PaketsLaufzeitumgebung für Queue- und Worker-Prozesse
Redis-Clientioredis, von BullMQ standardmäßig intern verwendet, sobald ein einfaches Verbindungsobjekt übergeben wirdVerbindung von Queue und Worker zur gemeinsamen Redis-Instanz
Schlüsselpräfix je AnwendungEigene, projektweit eindeutige Zeichenkette pro Anwendung über die BullMQ-Option prefix, Standardwert ohne eigene Angabe ist bullTrennt die Warteschlangen mehrerer Anwendungen in derselben Instanz
Monitoring-DashboardBull Board (npm-Pakete unter @bull-board), unterstützt laut eigener Dokumentation BullMQ ab Version 5.56.0 sowie die komplette Version 6Zeigt wartende, aktive, fehlgeschlagene und abgeschlossene Jobs über mehrere Queues hinweg an

Systemarchitektur

In einem Aufbau mit mehreren Anwendungen legt jede Anwendung ihre Jobs über eine eigene Queue-Instanz an, aber alle Queues schreiben in dieselbe Redis-Instanz. Ruft Anwendung A queue.add() auf, landet der Job als Datensatz in Redis, versehen mit dem Präfix dieser Anwendung, und der Aufruf gibt die Kontrolle sofort zurück, ohne auf das Ergebnis der eigentlichen Arbeit zu warten. Ein zur selben Queue gehörender Worker, ein separater Prozess mit derselben Verbindungskonfiguration und demselben Präfix, holt den Job ab und führt die eigentliche Funktion aus, etwa den E-Mail-Versand oder die Bildverkleinerung. Schlägt die Verarbeitung fehl, zählt BullMQ den Versuch, wartet die konfigurierte Backoff-Zeit ab und reiht den Job erneut ein, bis entweder die Verarbeitung gelingt oder die maximale Anzahl an Versuchen erreicht ist. Bleibt ein Worker mitten in der Verarbeitung stumm, weil sein Prozess abgestürzt ist oder den Event-Loop blockiert, erkennt BullMQ das an einem ausbleibenden Lebenszeichen und reiht den Job als stalled erneut ein, statt ihn stillschweigend verloren zu geben.

flowchart TD
    A[Anwendung A ruft queue.add auf] --> R[Gemeinsame Redis-Instanz]
    B[Anwendung B ruft queue.add auf] --> R
    C[Anwendung C ruft queue.add auf] --> R
    R --> S[Job liegt unter seinem Anwendungspraefix in der Warteschlange]
    S --> T[HTTP-Antwort der aufrufenden Anwendung kehrt sofort zurueck]
    S --> U[Zustaendiger Worker holt den Job zur Verarbeitung]
    U --> V{Verarbeitung erfolgreich}
    V -->|Ja| W[Job wechselt in den Zustand completed]
    V -->|Nein| X[BullMQ zaehlt einen Versuch und wartet die Backoff-Zeit ab]
    X --> Y{Attempts-Grenze erreicht}
    Y -->|Nein| U
    Y -->|Ja| Z[Job wechselt endgueltig in den Zustand failed]
    U --> AA[Worker meldet in festen Abstaenden ein Lebenszeichen]
    AA --> AB{Lebenszeichen bleibt laenger als 30 Sekunden aus}
    AB -->|Ja| AC[Job gilt als stalled und wird zurueck in die Warteschlange gelegt]

Die drei Anwendungen A, B und C teilen sich denselben Redis-Prozess und denselben Arbeitsspeicher, bleiben aber durch ihr jeweiliges Präfix logisch getrennt: Ein Wartungsskript, das nur die Schlüssel von Anwendung B durchsucht, sieht nie die Jobs von A oder C. Ein Neustart oder Ausfall der Redis-Instanz betrifft zwangsläufig alle drei Anwendungen gleichzeitig, weil sie sich denselben Prozess teilen. Ob dieser Neustart auch ihre Jobs kostet, hängt aber nicht von BullMQ ab, sondern ausschließlich davon, wie die Persistenz dieser einen gemeinsamen Redis-Instanz konfiguriert ist.

Schritt-für-Schritt-Umsetzung

Eine Queue anlegen und einen Job hinzufügen

Eine Queue-Instanz benötigt einen Namen und eine Verbindung zur gemeinsamen Redis-Instanz. BullMQ nutzt dafür standardmäßig den Redis-Client ioredis, den die Bibliothek intern selbst aufbaut, sobald ein einfaches Verbindungsobjekt mit Host und Port übergeben wird. Der Aufruf queue.add() legt einen Job mit einem Namen und einem Datenobjekt ab und kehrt zurück, sobald Redis den Job bestätigt hat, unabhängig davon, ob gerade ein Worker läuft oder nicht.

import { Queue } from 'bullmq';

const APP_PREFIX = 'app-d';

const emailQueue = new Queue('email-versand', {
  connection: {
    host: process.env.REDIS_HOST || 'localhost',
    port: Number(process.env.REDIS_PORT) || 6379,
  },
  prefix: APP_PREFIX,
});

async function enqueueWelcomeEmail(userId, email) {
  await emailQueue.add(
    'welcome-email',
    { userId, email },
    {
      attempts: 5,
      backoff: {
        type: 'exponential',
        delay: 2000,
      },
      removeOnComplete: 1000,
      removeOnFail: 5000,
    },
  );
}

Der HTTP-Request-Handler, der enqueueWelcomeEmail() aufruft, wartet nur auf das Ablegen des Jobs in Redis, nicht auf den tatsächlichen Versand der E-Mail. Die Option attempts: 5 erlaubt bis zu vier automatische Wiederholungen nach einem Fehlschlag, und removeOnComplete sowie removeOnFail begrenzen, wie viele abgeschlossene beziehungsweise endgültig fehlgeschlagene Jobs Redis als Verlauf aufhebt, statt die Warteschlange unbegrenzt wachsen zu lassen.

Einen Worker mit Wiederholung und Backoff starten

Ein Worker bekommt denselben Queue-Namen, eine Verarbeitungsfunktion und dieselbe Verbindungs- und Präfix-Konfiguration wie die zugehörige Queue. Wirft die Verarbeitungsfunktion einen Fehler oder gibt sie ein abgelehntes Promise zurück, wertet BullMQ das automatisch als Fehlschlag und wendet die beim Hinzufügen des Jobs konfigurierten attempts- und backoff-Einstellungen an, ohne dass der Worker selbst eine eigene Wiederholungslogik programmieren muss.

import { Worker } from 'bullmq';

const APP_PREFIX = 'app-d';

const emailWorker = new Worker(
  'email-versand',
  async (job) => {
    const { userId, email } = job.data;
    await sendWelcomeEmail(email);
    return { userId, sentTo: email };
  },
  {
    connection: {
      host: process.env.REDIS_HOST || 'localhost',
      port: Number(process.env.REDIS_PORT) || 6379,
    },
    prefix: APP_PREFIX,
    concurrency: 10,
  },
);

emailWorker.on('completed', (job) => {
  console.log(`Job ${job.id} erfolgreich verarbeitet`);
});

emailWorker.on('failed', (job, error) => {
  console.error(`Job ${job?.id} endgueltig fehlgeschlagen: ${error.message}`);
});

Mit backoff: { type: 'exponential', delay: 2000 } verdoppelt sich die Wartezeit vor jedem weiteren Versuch ausgehend von 2000 Millisekunden; die Alternative type: 'fixed' wartet dagegen vor jedem Versuch dieselbe, feste Zeitspanne. Die Option concurrency: 10 erlaubt demselben Worker-Prozess, bis zu zehn Jobs gleichzeitig zu bearbeiten, statt einen nach dem anderen abzuarbeiten.

Queues mehrerer Anwendungen per Präfix trennen

Läuft in derselben Anwendungslandschaft bereits eine zweite Anwendung mit eigenen Hintergrund-Jobs, zum Beispiel einer Bildverkleinerung, verbindet sie sich mit derselben Redis-Instanz, aber mit einem eigenen Wert für prefix. Ohne eigene Angabe verwenden alle Queues denselben Standardwert bull, wodurch Jobs unterschiedlicher Anwendungen unter identischen Schlüsseln landen würden, sobald sie zufällig denselben Queue-Namen wählen.

import { Queue, Worker } from 'bullmq';

const connection = {
  host: process.env.REDIS_HOST || 'localhost',
  port: Number(process.env.REDIS_PORT) || 6379,
};

const imageQueue = new Queue('bildverkleinerung', {
  connection,
  prefix: 'app-e',
});

const imageWorker = new Worker(
  'bildverkleinerung',
  async (job) => {
    const { imagePath } = job.data;
    await resizeImage(imagePath);
  },
  {
    connection,
    prefix: 'app-e',
    concurrency: 4,
  },
);

Queue und Worker müssen für dieselbe logische Warteschlange denselben Präfix tragen, sonst legt die Queue Jobs unter einem Schlüssel ab, den der zugehörige Worker nie abfragt, und die Jobs bleiben unverarbeitet liegen. Bull Board zeigt bei einer Verbindung zu mehreren Queues jede über ihren eigenen Präfix getrennt an, sodass sich wartende, aktive, fehlgeschlagene und abgeschlossene Jobs jeder Anwendung einzeln nachvollziehen lassen, ohne die Redis-Schlüssel von Hand mit SCAN durchsuchen zu müssen.

Häufige Fehlerquellen

  • Queue und Worker verwenden unterschiedliche Werte für prefix. Legt die Queue einer Anwendung Jobs unter dem Präfix app-d ab, während der zugehörige Worker ohne eigene Angabe mit dem Standardwert bull verbunden ist, sucht der Worker unter einem völlig anderen Satz von Redis-Schlüsseln. Symptom: Jobs häufen sich sichtbar in Redis an, ohne dass jemals ein completed- oder failed-Ereignis ausgelöst wird. Fix: Den Wert für prefix in einer einzigen Konfigurationsquelle je Anwendung halten und sowohl der Queue als auch jedem zugehörigen Worker exakt denselben Wert übergeben.
  • maxmemory-policy bleibt auf allkeys-lru stehen, weil die Instanz vorher nur als Cache lief. maxmemory-policy ist eine einzige, instanzweite Redis-Einstellung ohne Ausnahme je Datenbank oder Schlüsselmuster; steht sie auf allkeys-lru, entfernt Redis unter Speicherdruck auch wartende oder verzögerte BullMQ-Jobs, sobald deren Schlüssel am längsten nicht gelesen wurden. Laut BullMQ-eigener Produktionsdokumentation ist noeviction die einzige Einstellung, die korrektes Verhalten der Warteschlangen garantiert. Symptom: Einzelne Jobs verschwinden ersatzlos, ohne ein failed-Ereignis auszulösen, meist gerade dann, wenn die Instanz ohnehin schon unter Last steht. Fix: maxmemory-policy instanzweit auf noeviction umstellen, sobald BullMQ auf einer bislang rein als Cache genutzten Instanz mitläuft, und maxmemory großzügiger bemessen, weil Redis jetzt keine Schlüssel mehr eigenständig aufräumt.
  • Ein selbst erzeugter ioredis-Client für den Worker setzt maxRetriesPerRequest nicht auf null. BullMQ verlangt für Worker-Verbindungen laut eigener Produktionsdokumentation ausdrücklich den Wert null für diese ioredis-Option, weil Worker auf blockierende Redis-Befehle angewiesen sind, die ein anderer Wert unterbricht. Symptom: Der Worker wirft bei kurzen Redis-Verbindungsstörungen Ausnahmen, die mit einer gewöhnlichen Netzwerkstörung nichts zu tun haben, und verarbeitet währenddessen keine Jobs. Fix: Für Worker entweder ein einfaches Verbindungsobjekt an BullMQ übergeben, das die Bibliothek selbst mit den passenden Voreinstellungen aufbaut, oder bei einer selbst erzeugten ioredis-Instanz maxRetriesPerRequest: null explizit setzen.
  • Kein attempts-Wert über 1 gesetzt, obwohl automatische Wiederholung erwartet wird. Ohne eigene Angabe verarbeitet BullMQ einen Job nur einmal; schlägt die Verarbeitung fehl, wechselt der Job direkt in den Zustand failed, ohne einen einzigen weiteren Versuch. Symptom: Ein einzelner kurzer Netzwerkfehler beim Aufruf einer externen API wirft den Job endgültig aus der Warteschlange, obwohl derselbe Aufruf eine Sekunde später funktioniert hätte. Fix: Für jeden Job, der von einer instabilen Ressource abhängt, attempts auf einen Wert größer 1 setzen und dazu eine backoff-Strategie angeben, meist exponential.
  • Persistenz bleibt auf der gemeinsamen Redis-Instanz deaktiviert. Hält die Instanz weder ein AOF-Protokoll noch aktuelle RDB-Snapshots, verliert sie ihren gesamten Datenbestand bei einem harten Neustart, weil Redis sämtliche Daten ausschließlich im Arbeitsspeicher hält. Symptom: Nach einem Neustart der Redis-Instanz sind sowohl wartende als auch bereits aktive Jobs aller angeschlossenen Anwendungen gleichzeitig verschwunden, ohne dass jemals ein failed-Ereignis ausgelöst wurde. Fix: Für eine Instanz, die BullMQ-Warteschlangen trägt, mindestens eine AOF-Persistenz mit einer Schreibfrequenz von etwa einer Sekunde aktivieren, wie es die BullMQ-eigene Produktionsdokumentation empfiehlt, statt sich auf die für einen reinen Cache oft ausreichenden Standardeinstellungen zu verlassen.
  • maxStalledCount und die Prüfdauer passen nicht zu tatsächlich lang laufenden Jobs. Blockiert eine Verarbeitungsfunktion den Node.js-Event-Loop länger als die standardmäßige Prüfdauer von 30 Sekunden, etwa durch eine rechenintensive Bildbearbeitung ohne eigene asynchrone Unterbrechungen, meldet der Worker kein Lebenszeichen mehr und BullMQ stuft den Job als stalled ein. Symptom: Ein Job, dessen Worker tatsächlich noch gesund arbeitet, wird mit der Fehlermeldung job stalled more than allowable limit endgültig als fehlgeschlagen markiert. Fix: Rechenintensive Verarbeitung in einen sandboxed Prozessor auslagern, den BullMQ als eigene Datei referenziert, statt sie synchron im Hauptprozess des Workers laufen zu lassen.

Häufig gestellte Fragen

Was unterscheidet eine Queue, einen Worker und einen Job in BullMQ voneinander?

Eine Queue, ein Worker und ein Job sind die drei zentralen Bausteine von BullMQ, und jeder von ihnen übernimmt eine andere Aufgabe. Die Queue ist die Datenstruktur in Redis, in der Jobs abgelegt werden und über die sich eine Warteschlange pausieren, leeren oder abfragen lässt. Ein Worker ist ein oder mehrere Prozesse, die Jobs aus genau dieser Queue entgegennehmen und verarbeiten, wobei mehrere Worker gleichzeitig im selben Node.js-Prozess, in getrennten Prozessen oder auf getrennten Maschinen laufen dürfen. Ein Job ist die einzelne, benutzerdefinierte Dateneinheit mit ihrem eigenen Datenpaket und ihrem eigenen Lebenszyklus von wartend über aktiv bis abgeschlossen oder fehlgeschlagen.

Wie verhindert BullMQ, dass ein fehlgeschlagener Job einfach verloren geht?

BullMQ verhindert den Verlust eines fehlgeschlagenen Jobs über die beim Hinzufügen konfigurierbaren Optionen attempts und backoff. Mit attempts größer 1 reiht BullMQ einen Job nach einem Fehlschlag automatisch erneut ein, statt ihn sofort als endgültig gescheitert zu markieren. Die backoff-Option legt fest, wie lange BullMQ vor dem nächsten Versuch wartet, entweder fest mit dem Typ fixed oder mit wachsendem Abstand zwischen den Versuchen mit dem Typ exponential. Erst wenn die konfigurierte Anzahl an Versuchen ausgeschöpft ist, wechselt der Job endgültig in den Zustand failed, und selbst dann bleibt er als Datensatz in Redis nachvollziehbar.

Was passiert mit Jobs in der Warteschlange, wenn die gemeinsame Redis-Instanz neu startet?

Jobs in der Warteschlange überstehen einen Neustart der gemeinsamen Redis-Instanz nur, wenn diese Instanz mit aktivierter Persistenz läuft, weil Redis seinen gesamten Datenbestand sonst ausschließlich im Arbeitsspeicher hält. Mit aktiviertem AOF-Protokoll oder aktuellen RDB-Snapshots lädt Redis beim Neustart den zuletzt gesicherten Stand, und wartende wie bereits in Bearbeitung befindliche Jobs bleiben erhalten, wobei ein aktiver Job ohne Bestätigung als stalled erneut eingereiht wird. Ohne jede Persistenz gehen dagegen sämtliche Jobs aller an die Instanz angeschlossenen Anwendungen gleichzeitig verloren, unabhängig davon, wie sorgfältig attempts und backoff konfiguriert waren. Die Zuverlässigkeit von BullMQ reicht damit nie weiter als die Persistenzkonfiguration der Redis-Instanz darunter.

Woran erkennt BullMQ, dass ein Worker mitten in der Verarbeitung eines Jobs abgestürzt ist?

BullMQ erkennt einen mitten in der Verarbeitung abgestürzten Worker daran, dass dessen Job kein Lebenszeichen mehr sendet, und stuft ihn als stalled ein. Ein aktiver Job muss der Queue laufend bestätigen, dass sein Worker noch arbeitet; bleibt diese Bestätigung länger als die standardmäßige Prüfdauer von 30 Sekunden aus, etwa weil der Prozess abgestürzt ist oder in einer Endlosschleife hängt, reiht BullMQ den Job automatisch zurück in die Warteschlange. Überschreitet ein Job dabei die über maxStalledCount konfigurierte Anzahl an Stall-Ereignissen, markiert BullMQ ihn endgültig als fehlgeschlagen, mit der Fehlermeldung job stalled more than allowable limit, statt ihn unbegrenzt weiter zu versuchen.

Quellen

  1. BullMQ Releases: aktuelle Version 6.3.0 und Release-Historie, BullMQ-Projekt (GitHub, taskforcesh), abgerufen am 2026-08-26
  2. Queues-Leitfaden: new Queue() und queue.add(), BullMQ-Dokumentation, abgerufen am 2026-08-26
  3. Workers-Leitfaden: new Worker() und die Verarbeitungsfunktion, BullMQ-Dokumentation, abgerufen am 2026-08-26
  4. Retrying failing jobs: attempts- und backoff-Optionen (fixed/exponential), BullMQ-Dokumentation, abgerufen am 2026-08-26
  5. Connections-Leitfaden: prefix-Option und Standard-Redis-Client ioredis, BullMQ-Dokumentation, abgerufen am 2026-08-26
  6. Stalled Jobs: Erkennung ueber Lebenszeichen und maxStalledCount, BullMQ-Dokumentation, abgerufen am 2026-08-26
  7. Going to Production: Pflicht zu maxmemory-policy noeviction und AOF-Persistenz, BullMQ-Dokumentation, abgerufen am 2026-08-26
  8. Bull Board: Dashboard fuer Bull- und BullMQ-Warteschlangen, Bull Board-Projekt (GitHub, felixmosh), abgerufen am 2026-08-26

Der Content wurde mit Hilfe von KI erstellt, vor allem in der Recherche und Vorformulierung. Prüfung und Abnahme durch unsere Redaktion.