Ein Hintergrund-Job ist eine Aufgabe außerhalb des Anfragezyklus – von der App eingereiht, von Workern ausgeführt, bei Fehlern wiederholt. Die Trennlinie ist die Zeit: Eine Antwort soll sich sofort anfühlen (ein paar hundert Millisekunden) und muss die Gateway-Zeitüberschreitung schlagen (typischerweise etwa 30 Sekunden), während echte Arbeit – E-Mails über SMTP, Bildverarbeitung, Berichtserstellung, APIs von Drittanbietern – Sekunden bis Minuten dauert und Wiederholungsversuche verdient. Alles auf der falschen Seite dieser Linie wird eingereiht, bestätigt und anderswo erledigt; in großem Maßstab ist das Kerninfrastruktur und keine Klempnerei – ein großer Messaging-Anbieter verarbeitet täglich über eine Milliarde Jobs durch genau diese Maschinerie.
Das Wichtigste im Überblick
| Frage | Antwort |
|---|---|
| Die Architektur | Produzent reiht ein → Queue speichert dauerhaft → Worker führt aus und bestätigt |
| Die Trennlinie | Antwortbudget etwa 300 ms; alles Langsame oder Wiederholbare wird ein Job |
| Die drei Zeitmodi | Sofort · verzögert (“in 24 h”) · wiederkehrend (cron) |
| Der Zuverlässigkeitsvertrag | Mindestens einmal + idempotente Jobs + Backoff mit Jitter + Dead Letters |
| Das stille Scheitern | Nichts meldet einen Fehler, wenn ein geplanter Job ausbleibt – überwachen Sie die Abwesenheit |
Das Anti-Muster, das alles lehrt
// ✗ Die Registrierung, die sich einem Mailserver ausliefert
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await sendWelcomeEmail(user); // SMTP langsam? Der Nutzer starrt auf den Spinner.
res.json(user); // SMTP tot? Registrierung liefert 500 – aber das
}); // Konto EXISTIERT. Schlimmer geht es nicht.
// ✔ Einreihen und antworten
app.post('/signup', async (req, res) => {
const user = await createUser(req.body);
await jobs.enqueue('welcomeEmail', { userId: user.id }); // Millisekunden
res.json(user); // Die E-Mail geht raus, wiederholt sich
}); // und gelingt im eigenen Takt – unsichtbar
Dasselbe Prinzip von beiden Seiten eines echten Backends – ein Job über den Daten und ein Client, der einreiht statt zu warten:
// JavaScript — Cloud Code (cloud/main.js)
// A background job: heavy work outside the request cycle
Parse.Cloud.job('sendWeeklyDigest', async (request) => {
const query = new Parse.Query(Parse.User).equalTo('digestOptIn', true);
await query.eachBatch(async (users) => {
for (const user of users) await sendDigest(user); // per-record, resumable
}, { useMasterKey: true });
request.message('Digest run complete'); // status visible in the dashboard
});
// Run on demand or on a schedule from the dashboard — no queue to operate // Flutter / Dart — Back4app Flutter SDK
// The client's rule: never wait on heavy work — enqueue and move on
final export = ParseObject('ExportRequest')
..set('user', currentUser)
..set('status', 'queued'); // an afterSave trigger starts the work
await export.save(); // returns in milliseconds
// Watch the job's progress like any other data:
final sub = await LiveQuery().client.subscribe(
QueryBuilder<ParseObject>(ParseObject('ExportRequest'))
..whereEqualTo('objectId', export.objectId));
sub.on(LiveQueryEvent.update, (job) {
if (job.get<String>('status') == 'done') openReport(job.get('fileUrl'));
}); // iOS / Swift — Back4app Swift SDK
// The client's rule: never wait on heavy work — enqueue and move on
var export = ExportRequest()
export.status = "queued" // an afterSave trigger starts the work
let saved = try await export.save() // returns in milliseconds
// Watch the job's progress like any other data:
let sub = try await ExportRequest.query("objectId" == saved.id).subscribe()
sub.handleEvent { _, event in
if case .updated(let job) = event, job.status == "done" {
openReport(job.fileUrl)
}
} // Android / Kotlin — Back4app Android SDK
// The client's rule: never wait on heavy work — enqueue and move on
val export = ParseObject("ExportRequest")
export.put("user", ParseUser.getCurrentUser())
export.put("status", "queued") // an afterSave trigger starts the work
export.save() // returns in milliseconds
// Watch the job's progress like any other data:
val q = ParseQuery.getQuery<ParseObject>("ExportRequest")
q.whereEqualTo("objectId", export.objectId)
val sub = ParseLiveQueryClient.Factory.getClient().subscribe(q)
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, job ->
if (job.getString("status") == "done") openReport(job.getString("fileUrl"))
} Produzent, Queue, Worker
Drei Rollen, ein Vertrag. Der Produzent – meist ein Request-Handler – erzeugt einen Job: einen Typnamen und einen kleinen Payload. Die Queue speichert ihn dauerhaft; die Dauerhaftigkeit ist der ganze Punkt, denn Arbeit, die nur im Speicher eines sterbenden Prozesses existiert, stirbt mit ihm. Worker holen, führen aus und bestätigen (ack); ein Job ist erst weg, wenn er bestätigt ist – so überlebt der Job eines abgestürzten Workers und läuft erneut. Das Vokabular, das mitreist: enqueue/dequeue, Acks, Visibility Timeouts, und die Unterscheidung, auf die man achten sollte – eine Message-Queue transportiert Daten zwischen Diensten (und kann an viele Abonnenten verteilen); eine Job-Queue führt Arbeit aus, genau einmal pro Job und Worker, mit eingebauten Wiederholungsversuchen und Status.
Die drei Zeitmodi – und eine kurze Cron-Einführung
Jedes Job-System setzt dieselben drei Zeitmodi um: sofort (jetzt einreihen, laufen lassen, sobald ein Worker frei wird), verzögert (jetzt einreihen, zu einem späteren Zeitpunkt fällig – Erinnerungen, Ablauf von Testphasen und der Wiederholungsmechanismus selbst) und wiederkehrend (ein Scheduler reiht nach Kalender neu ein). Wiederkehrende Zeitpläne schreibt man fast immer in den fünf Feldern von cron:
┌ Minute (0-59) ┌ Stunde (0-23) ┌ Tag des Monats ┌ Monat ┌ Wochentag
0 2 * * * → täglich um 02:00
*/15 * * * * → alle 15 Minuten
0 9 * * 1 → montags um 09:00
Zwei Minen für den Scheduler: Sommerzeitwechsel (02:30 verschwindet oder
tritt zweimal auf – planen Sie in UTC) und Überlappung (ein Lauf, der sein
Intervall überdauert, braucht eine Sperre, sonst verarbeiten zwei Kopien
dieselben Daten).
Job-Queues vs. Message-Queues vs. Scheduler
| Job-Queue | Message-Queue | Scheduler | |
|---|---|---|---|
| Einheit | Ein auszuführender Job | Eine zuzustellende Nachricht | Ein Auslösezeitpunkt |
| Konsumenten | Genau ein Worker | Einer oder viele Abonnenten | Die Jobs, die er einreiht |
| Eingebaut | Wiederholungen, Status, Prioritäten, Verzögerung | Dauerhaftigkeit, Routing, Fan-out | Kalender, Wiederholung |
| Open-Source-Namen | Sidekiq, Celery, BullMQ | RabbitMQ, Kafka, Redis | cron, Quartz |
| Fügt sich ein als | Führt aus, was Ereignisse verlangen | Transportiert zwischen Systemen | Speist die Queue pünktlich |
Die Komposition beantwortet die meisten “Welches davon?”-Debatten: Scheduler entscheiden wann, Queues halten was, Worker erledigen das Tun – und ein nächtlicher Batch ist cron, das Jobs einreiht, die Worker mit der vollen Wiederholungssemantik ausführen.
Der Zuverlässigkeitsvertrag
Die Teile werden meist getrennt gelehrt; sie sind ein einziger Vertrag. Die Queue sichert mindestens einmal zu – ein Worker, der nach der Arbeit, aber vor der Bestätigung abstürzt, bedeutet, dass der Job erneut läuft; das ist unvermeidlich, nicht schlampig. Sie sichern im Gegenzug Idempotenz zu: nach Job-ID deduplizieren, mit Eindeutigkeits-Constraints absichern, mit Upserts schreiben – damit der zweite Lauf ein Leerlauf ist statt einer zweiten Rechnung. Wiederholungsversuche mit exponentiellem Backoff und Jitter überbrücken vorübergehende Störungen – 1 s, 2 s, 4 s, mit Zufallsanteil, damit tausend gemeinsam gescheiterte Jobs nicht gemeinsam wiederholen, eine Höflichkeit, die ratenbegrenzte Drittanbieter erzwingen, wenn Sie sie nicht von sich aus erweisen. Die Dead Letter Queue fängt den Rest ab: Nach der Versuchsobergrenze parken Jobs dort, wo Menschen sie prüfen, reparieren und erneut abspielen können – und dauerhaft fehlerhafte Jobs sollten das Wiederholungstheater ganz überspringen und direkt dorthin gehen.
Jobs gut entwerfen
Die Regeln, auf die sich die Queue-Frameworks einigen, zusammengetragen: IDs übergeben, keine Objekte – ein Payload mit userId: "u-8fk2" holt zur Laufzeit frischen Zustand, während ein serialisiertes Nutzerobjekt schon im Moment des Einreihens veraltet ist; Payloads klein halten und JSON-einfach, niemals Secrets; ein Job pro Datensatz – ein Job je Nutzer wiederholt bei einem Fehler einen Nutzer, ein Mega-Job wiederholt alle; lange Arbeit fortsetzbar machen – in Blöcke zerlegen, Fortschritt festhalten und bei Shutdown-Signalen mitten im Block sauber aussteigen; und von Nebenläufigkeit ausgehen – irgendwann verarbeiten zwei Worker benachbarte Jobs auf derselben Zeile, und das ist eine Frage der Sperren, die Sie beim Entwurf oder im Störfall beantworten.
Die Queue im Blick behalten
Scheitern im Hintergrund ist still – kein Nutzer sieht eine Fehlerseite, wenn der Digest-Job stirbt. Die vier Messgrößen, die die Fehlerseite ersetzen: Queue-Tiefe (ein wachsender Rückstand heißt, die Worker verlieren), Job-Alter gemessen vom Einreihen bis zum Abschluss – ein Job, der nach 40 Minuten Wartezeit in 200 ms verarbeitet wird, ist für den Nutzer ein 40-Minuten-Job; Fehler- und Dead-Letter-Raten mit Alarmen auf der Dead Letter Queue, denn geparkte Jobs, die jemand vergessen hat, sind Daten, die still nicht verarbeitet werden; und ausgebliebene Läufe bei geplanter Arbeit – der besonders heimtückische Fall, denn ein Zeitplan, der nie gefeuert hat, erzeugte keinen Fehler, keine Logzeile und überhaupt keinen Job. Alarmieren Sie auf die Abwesenheit.
Typische Anwendungsfälle
- E-Mail und Benachrichtigungen – die klassische Verschiebung: bei der Registrierung einreihen, mit Wiederholungen zustellen.
- Medienverarbeitung – Skalieren, Transkodieren, Vorschaubilder: Minuten an CPU-Zeit, auf die keine Anfrage warten sollte.
- Berichte und Exporte – im Hintergrund erzeugen, benachrichtigen, sobald die Datei bereit ist.
- Synchronisation mit Drittanbietern – CRM-Abgleiche und Webhook-Zustellungen, mit Backoff gegen wackelige Gegenstellen.
- Bereinigung und Wartung – TTL-Durchläufe, Entfernen von Waisen, Digest-Erstellung im nächtlichen Cron-Job.
Welches Muster brauchen Sie? Entscheidungsmatrix
| Die Arbeit sieht so aus | Greifen Sie zu |
|---|---|
| Von Nutzeraktionen ausgelöst, schwankendes Volumen | Job-Queue + Worker |
| Fester Kalender, begrenzt, vorhersehbar | Geplanter Job (cron) |
| Nächtlicher Batch über viele Datensätze | Cron reiht ein; Worker führen pro Datensatz aus |
| Dienste, die einander etwas mitteilen | Message-Queue / Pub-Sub |
| Heute langsame Arbeit im Request-Handler | Das Refactoring von oben – einreihen und antworten |
| Muss Abstürze überstehen und sicher wiederholen | Alles davon – plus der Zuverlässigkeitsvertrag |
Grenzen und Trade-offs
- Letztendlich, nicht sofort. Eingereihte Arbeit passiert später – Oberflächen brauchen Wartezustände, und “später” braucht eine Obergrenze, die jemand bewusst gewählt hat.
- Zustand wandert aus dem Takt. Ergebnisse kommen über Statusfelder, Callbacks oder Benachrichtigungen – die Einfachheit von Anfrage und Antwort ist verbraucht, planen Sie die Verkabelung ein.
- Infrastruktur ist real. Broker, Worker und Scheduler sind Dienste mit eigenen Fehlermodi – oder das Problem einer Plattform, was das Argument für verwaltete Jobs ist.
- Duplikate sind irgendwann garantiert. “Mindestens einmal” ist der Vertrag; jeder nicht idempotente Job ist ein Störfall mit Zeitzünder.
- Queues verbergen Überlast elegant – zu elegant. Ein wachsender Rückstand wirkt ruhig, bis das Job-Alter explodiert; Alarme auf Tiefe und Alter sind der Ehrlichkeitsmechanismus.
Hintergrund-Jobs mit Back4app
Back4app ist eine Open-Source-Plattform für Backend as a Service (BaaS), die eine verwaltete Datenbank, automatisch generierte REST- und GraphQL-APIs, Authentifizierung, Dateispeicher und Serverless-Funktionen mit Cloud Code kombiniert. Jobs sind hier Cloud Code mit einem Zeitplan: Definieren Sie Parse.Cloud.job – die Code-Tabs zeigen einen Digest-Job, der mit eachBatch über Nutzer hinweg in Blöcken arbeitet, pro Datensatz und fortsetzbar – und führen Sie ihn bei Bedarf oder in einer Wiederholung im Cron-Stil aus dem Dashboard aus, mit Status und Logs im Jobs-Panel; kein Broker bereitzustellen, keine Worker-Flotte zu skalieren, kein Queue-Dienst am Leben zu halten. Die ereignisgesteuerte Hälfte setzt sich aus denselben Bausteinen zusammen: Ein Request-Handler schreibt eine Zeile mit status: "queued", ein afterSave-Trigger startet die Arbeit, und Clients verfolgen den Fortschritt über Live Queries, statt zu pollen – das Produzent-Worker-Muster als Daten ausgedrückt, mit dem Zuverlässigkeitsvertrag (IDs im Payload, idempotente Handler, Status, auf den Sie alarmieren können) als Ihrer Entwurfsdisziplin statt als Ihrer Infrastruktur.
Häufige Fragen
Was ist ein Hintergrund-Job?
Eine Aufgabe, die Ihre Anwendung außerhalb des Anfrage-Antwort-Zyklus ausführt: Der Server reiht die Arbeit in eine Queue ein, antwortet dem Nutzer sofort, und ein separater Worker führt sie asynchron aus – mit Wiederholungsversuchen, falls sie scheitert. Alles Langsame, Wiederholbare oder für die Antwort Unnötige gehört dorthin.
Warum die Arbeit nicht einfach im Request-Handler erledigen?
Weil langsame Arbeit die Antwort blockiert, Serverkapazität bindet und in Gateway-Zeitüberschreitungen läuft – und wenn sie mitten in der Anfrage scheitert, bekommt der Nutzer einen Fehler, obwohl ein Teil bereits passiert ist. Das klassische Beispiel: die Willkommens-E-Mail während der Registrierung, wo ein langsamer Mailserver die Kontoerstellung in ein drehendes Rädchen verwandelt.
Wie funktioniert eine Job-Queue?
Sie ist eine dauerhafte Liste zwischen Produzenten und Workern: Die App legt einen Job ab – einen Typ plus einen kleinen Payload –, die Queue speichert ihn dauerhaft, und Worker holen ihn, führen ihn aus und bestätigen ihn. Unbestätigte Jobs kehren in die Queue zurück, und genau daher kommt die Zuverlässigkeit.
Was ist der Unterschied zwischen einer Job-Queue und einer Message-Queue?
Der Zweck. Eine Message-Queue transportiert Daten zwischen Diensten – die Zustellung ist das Ziel, und eine Nachricht kann an viele Konsumenten gehen. Eine Job-Queue führt Arbeit aus – sie legt Wiederholungsversuche, Planung, Prioritäten und Status obendrauf, und jeder Job geht an genau einen Worker.
Was ist der Unterschied zwischen Cron-Jobs und Hintergrund-Jobs?
Der Auslöser. Cron ist zeitgesteuert – "jede Nacht um 2 Uhr"; Hintergrund-Jobs sind ereignisgesteuert – "wenn ein Nutzer eine Datei hochlädt". Die Muster ergänzen sich: Ein verbreiteter Entwurf lässt cron den nächtlichen Batch einreihen, während Worker ihn mit der vollen Wiederholungssemantik ausführen.
Wie funktionieren Wiederholungsversuche bei Jobs?
Gescheiterte Jobs laufen automatisch erneut, mit exponentiellem Backoff – eine Sekunde, dann zwei, vier, acht – plus zufälligem Jitter, damit tausend Fehlschläge nicht alle im selben Moment wiederholen, begrenzt durch eine maximale Versuchszahl. Backoff überbrückt vorübergehende Störungen; die Obergrenze verhindert, dass dauerhafte Fehler ewig wiederholt werden.
Warum müssen Hintergrund-Jobs idempotent sein?
Weil Queues eine Ausführung mindestens einmal zusichern: Ein Worker kann abstürzen, nachdem er die Arbeit erledigt, aber bevor er sie bestätigt hat – und der Job läuft erneut. Zweimal laufen darf nicht zweimal abrechnen; Idempotenzschlüssel, Eindeutigkeits-Constraints und Upserts machen aus Duplikaten Leerläufe.
Was ist eine Dead Letter Queue?
Dorthin wandern Jobs, die ihre Wiederholungsversuche aufgebraucht haben – aufbewahrt zur Prüfung, zur Alarmierung und zum manuellen erneuten Abspielen, statt ewig zu wiederholen oder still zu verschwinden. Fehlerhaft aufgebaute Jobs, die nie gelingen können, sollten die Versuche überspringen und direkt dort landen.