
Laravel Background Jobs Queues für extreme Geschwindigkeit

Wenn du im modernen Web überleben willst, sind Laravel Background Jobs Queues deine absolute Geheimwaffe für eine kompromisslose User Experience. Wir haben in den letzten zwölf Kapiteln eine Architektur gebaut, die auf pure Geschwindigkeit ausgelegt ist. Dein Next.js-Frontend rendert in Millisekunden, und Laravel liefert die Daten dank Redis aus dem Arbeitsspeicher, ohne dass die Datenbank auch nur mit der Wimper zuckt.
Aber wir haben ein massives architektonisches Problem bisher völlig ignoriert. Eine API ist immer nur so schnell wie ihr langsamstes Glied. Was passiert eigentlich, wenn unser Server wirklich harte, zeitintensive Arbeit verrichten muss?
Lass uns ein realistisches Szenario aus dem Redaktionsalltag deines neuen Enterprise-CMS durchspielen: Du hast gerade einen fantastischen, tiefgründigen Artikel geschrieben. Du sitzt in deinem Admin-Dashboard, klickst auf den Button "Artikel veröffentlichen" und lehnst dich zurück. Was in diesem Moment technisch im Hintergrund passiert, ist gewaltig.
Dein Laravel-Server muss den Artikel in der MySQL-Datenbank speichern. Er muss den alten Redis-Cache über den Observer löschen. Aber das ist noch nicht alles: Du hast 5.000 treue Newsletter-Abonnenten, die sofort per E-Mail über den neuen Beitrag informiert werden sollen. Gleichzeitig hast du ein hochauflösendes 10-Megabyte-Titelbild hochgeladen, das der Server erst noch in drei verschiedene WebP-Größen (Thumbnail, Medium, Large) umrechnen muss, um Bandbreite zu sparen.
Wenn du das auf die klassische, synchrone Art in PHP programmierst, zwingst du deinen Server, diese Aufgaben nacheinander (sequenziell) abzuarbeiten, während der Browser deines Nutzers verzweifelt auf eine Antwort wartet.
Der Server rechnet die Bilder um – das dauert 4 Sekunden. Der Server verbindet sich mit dem SMTP-Provider (z.B. Mailgun oder Postmark) und versucht, 5.000 E-Mails einzeln zu verschicken. Jede E-Mail dauert 200 Millisekunden. Das sind 1.000 Sekunden Wartezeit.
Dein armer Browser-Tab wird sich so lange mit einem eingefrorenen Lade-Spinner drehen, bis der Nginx-Webserver nach 60 Sekunden entnervt aufgibt und dir einen hässlichen 504 Gateway Timeout Fehler vor die Füße wirft. Die Nutzererfahrung ist eine absolute Katastrophe. All die unglaubliche Geschwindigkeit, die wir mit React Server Components und Redis aufgebaut haben, ist völlig wertlos, wenn wir den Nutzer zwingen, auf langsame Prozesse zu warten.
Die Architektur der Profis: Entkopplung
Genau hier betreten wir die echte Profiliga der Backend-Entwicklung. Die Lösung für dieses Problem lautet: Asynchrone Prozesse. Wir müssen die schnelle HTTP-Antwort von der schweren Arbeit strikt entkoppeln.
Wenn du in Zukunft auf "Veröffentlichen" klickst, wird Laravel die Bilder nicht mehr sofort berechnen und die E-Mails nicht mehr sofort verschicken. Stattdessen packt es diese Aufgaben als kleine Notizzettel (die sogenannten "Jobs") in eine rasend schnelle Warteschlange (die "Queue").
Das Ablegen dieser Notizzettel dauert exakt 2 Millisekunden. Der Controller ist sofort fertig und antwortet dem Next.js-Frontend blitzschnell: "Alles klar, Artikel ist gespeichert! Mach dir keine Sorgen um den Rest, wir kümmern uns im Hintergrund darum." Der Lade-Spinner verschwindet sofort, du siehst eine grüne Erfolgsmeldung und kannst direkt den nächsten Artikel schreiben.
Während du schon wieder produktiv bist, erwachen tief im Maschinenraum deines Servers unsichtbare Arbeiter (die "Queue Worker") zum Leben. Sie schnappen sich die Notizzettel aus der Warteschlange und arbeiten sie stumm, effizient und völlig losgelöst vom Nutzererlebnis ab.
Das ist die Architektur, die Plattformen wie YouTube nutzen, wenn sie dir sagen: "Dein Video wird verarbeitet, wir benachrichtigen dich, wenn es fertig ist."

Die Kommandozentrale: Redis als rasende Warteschlange
Wir haben unseren roten Redis-Kristall im letzten Teil als blitzschnellen Zwischenspeicher (Cache) für unsere API kennengelernt. Aber Redis hat noch ein zweites, fast noch mächtigeres Gesicht: Es ist der absolute Goldstandard für das Verwalten von Warteschlangen, ein sogenannter "Message Broker".
Vielleicht fragst du dich jetzt: "Warum nehme ich für meine Warteschlange nicht einfach meine bestehende MySQL-Datenbank? Laravel bietet doch auch einen Datenbank-Treiber für Queues an!"
Das ist eine exzellente Frage, die in der Praxis oft zu schmerzhaften Lektionen führt. Wenn du eine relationale Datenbank als Queue nutzt, muss der Server bei jeder neuen Aufgabe einen Eintrag in eine Tabelle schreiben. Deine Hintergrund-Arbeiter (die Worker) müssen diese Tabelle dann im Millisekundentakt mit SELECT-Abfragen bombardieren, um zu schauen, ob es neue Arbeit gibt. Wenn ein Arbeiter eine Aufgabe findet, muss er die Zeile in der Datenbank blockieren (Row-Level Locking), damit kein zweiter Arbeiter dieselbe Aufgabe doppelt ausführt. Bei hohem Traffic führt genau das zu massiven Datenbank-Deadlocks. Deine MySQL-Datenbank bricht unter der reinen Verwaltung der Aufgaben zusammen, noch bevor die eigentliche Arbeit überhaupt begonnen hat.
Redis hingegen nutzt für Warteschlangen simple, rasend schnelle Listen im Arbeitsspeicher. Das Ablegen und Herausholen von Aufgaben passiert konfliktfrei in Bruchteilen einer Millisekunde.
Lass uns diese Kommandozentrale aktivieren. Da wir den Redis-Server in Teil 12 bereits installiert haben, ist die Konfiguration ein absolutes Kinderspiel. Öffne deine .env-Datei und ändere genau eine einzige Zeile:
# Wir werfen den langsamen, synchronen Treiber raus und schalten den Turbo ein
QUEUE_CONNECTION=redisSpeichere die Datei und leere deinen Konfigurations-Cache mit php artisan config:clear. Dein Laravel-System ist jetzt offiziell asynchron.
Den ersten Notizzettel schreiben: Die Job-Klasse
Jetzt, wo das Förderband läuft, müssen wir lernen, wie wir die Aufgaben richtig verpacken. In Laravel repräsentiert eine sogenannte "Job-Klasse" genau eine einzige, in sich geschlossene Aufgabe.
Lass uns das Beispiel mit der Bildkomprimierung aus der Einleitung aufgreifen. Wir wollen, dass der Server das hochgeladene 10-Megabyte-Titelbild im Hintergrund in web-optimierte WebP-Formate umrechnet, ohne dass der Redakteur darauf warten muss.
Öffne dein Terminal und generiere deinen allerersten Job:
php artisan make:job OptimizeArticleImagesLaravel legt nun im Ordner app/Jobs eine brandneue Datei für dich an. Schau dir die Struktur dieser OptimizeArticleImages.php an. Sie ist das Herzstück unserer asynchronen Architektur:
1<?php
2
3namespace App\Jobs;
4
5use App\Models\Post;
6use Illuminate\Bus\Queueable;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Bus\Dispatchable;
9use Illuminate\Queue\InteractsWithQueue;
10use Illuminate\Queue\SerializesModels;
11
12// Das Interface "ShouldQueue" ist der absolute Schlüssel!
13// Es sagt Laravel: "Führe mich niemals sofort aus, sondern leg mich auf das Förderband!"
14class OptimizeArticleImages implements ShouldQueue
15{
16 use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
17
18 public $post;
19
20 /**
21 * Der Konstruktor nimmt die notwendigen Daten für den Job entgegen.
22 * Wir übergeben hier unser Post-Model, das das riesige Bild enthält.
23 */
24 public function __construct(Post $post)
25 {
26 $this->post = $post;
27 }
28
29 /**
30 * HIER PASSIERT DIE EIGENTLICHE, SCHWERE ARBEIT!
31 * Diese Methode wird erst aufgerufen, wenn ein Roboter-Arm (Worker) den Job vom Band nimmt.
32 */
33 public function handle(): void
34 {
35 // Wir simulieren hier die schwere Arbeit der Bildkomprimierung.
36 // In der Realität würdest du hier z.B. die Spatie Media Library aufrufen.
37
38 sleep(4); // Wir zwingen den Server künstlich, 4 Sekunden hart zu arbeiten
39
40 // Danach speichern wir z.B. einen Status in der Datenbank
41 $this->post->update(['images_optimized' => true]);
42
43 // Da sich die Daten des Posts geändert haben, rufen wir unseren Observer
44 // auf den Plan, um den Redis-Cache für Next.js zu leeren!
45 }
46}Schau dir den Code genau an. Das absolut Wichtigste ist das Interface ShouldQueue oben in der Klassen-Definition. Das ist der magische Schalter. Wenn du eine Methode aufrufst, die dieses Interface implementiert, weiß Laravel sofort, dass es den Code in der handle()-Methode komplett ignorieren und den Job stattdessen direkt an Redis übergeben soll.
Der SerializesModels-Trait ist ebenfalls pure Magie. Wenn wir unser $post Objekt in den Konstruktor stecken, speichert Laravel nicht das gesamte gigantische Model inklusive aller Bilder im Redis-Speicher. Es speichert nur den Namen der Klasse (App\Models\Post) und die ID (z.B. 123). Wenn der Worker den Job später ausführt, holt er sich exakt dieses Model mit all seinen frischen Daten automatisch wieder aus der Datenbank.

Der magische Moment: Jobs asynchron abfeuern
Wir haben unsere Kommandozentrale eingerichtet und unseren fleißigen Notizzettel (OptimizeArticleImages) geschrieben. Jetzt kommt der Moment, auf den wir hingearbeitet haben. Wir binden diesen Job in unseren normalen API-Ablauf ein und entkoppeln die schwere Arbeit von der Nutzererfahrung.
Lass uns in den Controller gehen, der aufgerufen wird, wenn du im Admin-Dashboard auf "Artikel veröffentlichen" klickst. Nennen wir ihn einfach mal PostController.
Erinnerst du dich an unser Horror-Szenario? Wenn wir die Bildkomprimierung (die wir in unserem Job künstlich mit sleep(4) simuliert haben) hier direkt im Controller ausführen würden, müsste der Nutzer geschlagene vier Sekunden auf einen weißen Bildschirm starren, bevor der Server antwortet. Das ist in der heutigen Zeit ein absolutes No-Go.
Schau dir an, wie wir dieses Problem mit einer einzigen, unfassbar eleganten Zeile Code in Luft auflösen:
1<?php
2
3namespace App\Http\Controllers\Admin;
4
5use App\Http\Controllers\Controller;
6use App\Http\Requests\StorePostRequest;
7use App\Models\Post;
8use App\Jobs\OptimizeArticleImages;
9use Illuminate\Http\JsonResponse;
10
11class PostController extends Controller
12{
13 public function store(StorePostRequest $request): JsonResponse
14 {
15 // 1. Der Artikel wird blitzschnell validiert und in MySQL gespeichert
16 $post = Post::create($request->validated());
17
18 // 2. HIER PASSIERT DIE MAGIE!
19 // Wir befehlen Laravel NICHT, die Bilder jetzt zu berechnen.
20 // Wir werfen den Job einfach auf das rote Redis-Förderband.
21 OptimizeArticleImages::dispatch($post);
22
23 // 3. Die Antwort schießt sofort, ohne jede Verzögerung, an Next.js zurück!
24 return response()->json([
25 'message' => 'Artikel gespeichert! Bilder werden im Hintergrund optimiert.',
26 'data' => $post
27 ], 201);
28 }
29}Wenn du diesen Code jetzt ausführst und auf deinen "Veröffentlichen"-Button klickst, wirst du deinen Augen kaum trauen. Die Wartezeit ist von 4.000 Millisekunden auf unter 50 Millisekunden gedroppt. Dein Next.js-Frontend bekommt sofort das grüne Licht, leert das Formular und zeigt dem Redakteur eine Erfolgsmeldung an. Die User Experience ist absolut reibungslos.
Was passiert eigentlich im Hintergrund?
Während dein Redakteur sich schon einen Kaffee holt, spielt sich in den Eingeweiden deines Servers ein faszinierender Prozess ab.
Sobald Laravel das Wort dispatch() liest, stoppt es die normale Ausführung der Job-Klasse komplett. Es schaut sich das $post-Objekt an, das wir übergeben haben. Dank des SerializesModels-Traits extrahiert Laravel nur die ID des Artikels. Dann nimmt es den kompletten Job, verpackt ihn in einen winzigen JSON-String und wirft ihn in eine spezielle Liste in unserem Redis-Arbeitsspeicher.
Wenn du jetzt direkt in deine Redis-Instanz schauen würdest, sähest du dort ein Paket, das ungefähr so aussieht: "Hey, hier ist der Job OptimizeArticleImages. Wenn jemand Zeit hat, führt ihn bitte für den Post mit der ID 123 aus."
Das Problem ist nur: Aktuell hat niemand Zeit. Dein Controller hat seinen Job erledigt und sich beendet. Der Job liegt jetzt völlig einsam auf dem Förderband und wartet darauf, dass ihn jemand abholt. Die Bilder deines Artikels sind immer noch unkomprimiert.
Wir haben das Frontend zwar erfolgreich entlastet, aber die Arbeit muss trotzdem noch von irgendjemandem erledigt werden. Wir brauchen Arbeiter.

Das Heer der unsichtbaren Arbeiter: Queue Worker aktivieren
Lass uns rekapitulieren. Wir haben unsere Kommandozentrale (Redis) eingerichtet, wir haben unseren fleißigen Notizzettel (die Job-Klasse OptimizeArticleImages) geschrieben und wir haben unseren Controller so umgebaut, dass er die Jobs mit Überschallgeschwindigkeit asynchron abfeuert.
Das Problem ist nur: Aktuell haben wir zwar tausende von Notizzetteln auf dem roten Redis-Förderband liegen, aber niemanden, der sie liest und die Arbeit erledigt. Die Bilder deines Artikels sind immer noch unkomprimiert. Dein Newsletter wurde nicht verschickt. Wir haben das Frontend zwar entlastet, aber die eigentliche Last ist nur an einen anderen Ort verschoben worden.
Wir müssen jetzt unsere unsichtbaren Arbeiter – die sogenannten Queue Worker – aktivieren.
Ein Queue Worker ist nichts anderes als ein permanenter Hintergrundprozess (ein Daemon) auf deinem Server, der nichts anderes tut, als stur und unermüdlich auf das Redis-Förderband zu starren. Sobald ein neuer Job dort auftaucht, schnappt er ihn sich, führt die handle()-Methode aus und macht Platz für den nächsten.
Tippe diesen einen, fundamentalen Artisan-Befehl in ein zweites, separates Terminal-Tab ein, da dieser Prozess permanent laufen muss:
php artisan queue:workWenn du jetzt in dein Admin-Dashboard gehst und einen neuen Artikel veröffentlichst, wirst du in diesem zweiten Terminal-Tab ein absolutes Performance-Feuerwerk beobachten. Du wirst sehen, wie der Job dort auftaucht, wie Laravel ihn verarbeitet und wie er nach genau vier Sekunden (unserem sleep(4)) erfolgreich als Processed markiert wird.
BÄM! Deine Bilder sind optimiert. Dein Newsletter ist raus. Und das alles, während dein Redakteur im Frontend schon längst am nächsten Artikel arbeitet. Du hast gerade deine Architektur verdoppelt und die rohe Backend-Power von der User Experience entkoppelt.
Der ultimative Profi-Tipp: Code-Änderungen und der Worker-Cache
Ich garantiere dir, dass du in den nächsten Wochen unweigerlich in eine sehr schmerzhafte Falle tappen wirst. Das ist der Moment, in dem du an deinem eigenen Verstand zweifeln wirst.
Stell dir vor, du änderst den Code in deinem OptimizeArticleImages Job. Du änderst zum Beispiel das sleep(4) auf sleep(1), weil du die Bildkomprimierung beschleunigen willst. Du speicherst die Datei, veröffentlichst einen neuen Artikel und... der Worker braucht immer noch vier Sekunden. Du änderst den Text im E-Mail-Newsletter, aber deine Abonnenten erhalten immer noch den alten Text.
Warum zur Hölle ignoriert Laravel deine Änderungen?
Die Antwort liegt in der Funktionsweise des queue:work Befehls. Um maximale Performance zu erreichen, ist dieser Prozess extrem clever. Er bootet dein komplettes Laravel-Framework mit all seinen Konfigurationen, Models und Providern genau einmal – beim Start des Befehls. Danach hält er das gesamte System im Arbeitsspeicher (RAM). Wenn er einen Job vom Band nimmt, muss er Laravel nicht jedes Mal neu laden. Das ist pfeilschnell.
Aber das bedeutet auch: Wenn du deinen PHP-Code änderst, weiß der laufende Worker-Prozess absolut nichts davon. Er arbeitet stur mit der alten Version von Laravel weiter, die er vor Stunden in seinen RAM geladen hat.
Um deine Code-Änderungen in der Queue zu aktivieren, musst du den laufenden Worker-Prozess knallhart beenden und neu starten. In der Entwicklung machst du das oft manuell mit Strg+C und einem erneuten Aufruf. In der Produktion nutzen wir dafür einen speziellen Befehl:
php artisan queue:restartDieser Befehl ist chirurgisch präzise. Er beendet keine Jobs, die gerade in Bearbeitung sind. Er sendet nur ein stummes Signal an alle Worker, dass sie sich nach Erledigung ihrer aktuellen Aufgabe sofort beenden und neu starten sollen, um den frischen Code aus dem RAM zu laden.
Gewöhne dir diesen Befehl an. Er wird dir unzählige Stunden voller Frust und Verzweiflung ersparen.
Die dunkle Seite: Wenn Jobs krachen gehen (Failed Jobs)
Wir haben uns bisher in einer perfekten Welt bewegt. Der Code war fehlerfrei, der Server hatte unendlich viel Leistung und alle externen APIs haben in Millisekunden geantwortet. Aber lass uns ehrlich sein: Die reale Welt der Softwareentwicklung ist ein Schlachtfeld.
Was passiert eigentlich mit unseren schönen Laravel Background Jobs Queues, wenn die Realität zuschlägt?
Stell dir vor, unser OptimizeArticleImages Job läuft gerade auf dem Worker. Plötzlich bricht die Verbindung zu deinem S3-Speicher ab. Oder nehmen wir unser Newsletter-Szenario: Dein Worker versucht gerade, 5.000 E-Mails zu verschicken, aber dein SMTP-Provider (z. B. Mailgun) hat ein spontanes Server-Problem und antwortet mit einem 503 Service Unavailable.
In einer alten, synchronen PHP-Anwendung würde dein gesamtes Skript an dieser Stelle mit einer fatalen Exception abstürzen. Der Nutzer sähe eine weiße Seite mit einer Fehlermeldung, und die restlichen 4.900 E-Mails wären für immer verloren. Niemand wüsste genau, wer die Mail schon bekommen hat und wer nicht. Ein absoluter Albtraum.
In unserer neuen, asynchronen Architektur passiert etwas viel Eleganteres. Wenn ein Job eine Exception wirft, fängt der Queue Worker diesen Fehler auf. Das System stürzt nicht ab! Der Worker legt diesen speziellen Job einfach beiseite, markiert ihn als "fehlgeschlagen" und nimmt sich in der nächsten Millisekunde sofort den nächsten Job vom Förderband. Das System läuft unbeirrt weiter.
Der Rettungsanker: Die failed_jobs Tabelle
Aber wo legt der Worker diesen kaputten Notizzettel ab? Er wirft ihn nicht einfach weg. Seit Laravel 11 ist in den Standard-Datenbankmigrationen bereits eine spezielle Tabelle namens failed_jobs enthalten. Wenn ein Job endgültig scheitert, nimmt Laravel ihn aus dem flüchtigen Redis-RAM und speichert ihn samt der kompletten Fehlermeldung, dem Stacktrace und den ursprünglichen Daten sicher in deiner MySQL-Datenbank ab. Er wird gewissermaßen in Quarantäne gesteckt.
Du kannst dir all diese gestrandeten Jobs jederzeit im Terminal ansehen:
php artisan queue:failedDu siehst dort genau, wann welcher Job warum fehlgeschlagen ist. Sobald dein SMTP-Provider wieder online ist oder du den Bug in deinem Code repariert (und den Worker neu gestartet!) hast, kannst du Laravel befehlen, es einfach noch einmal zu versuchen:
php artisan queue:retry allLaravel nimmt alle fehlerhaften Jobs aus der MySQL-Datenbank, packt sie wieder als frische Pakete auf das Redis-Förderband, und die Worker versuchen ihr Glück ein zweites Mal. Du hast kein einziges Datenpaket verloren!
Automatische Wiederholungsversuche (Retries & Backoff)
Manuelles Eingreifen über das Terminal ist gut für Notfälle, aber in einem echten Enterprise-System wollen wir Automatisierung. Oft sind Netzwerkfehler nur extrem kurze Aussetzer. Wenn ein API-Call fehlschlägt, reicht es meistens, einfach 30 Sekunden zu warten und es noch einmal zu probieren.
Genau das bringen wir unserem Job jetzt bei. Öffne deine OptimizeArticleImages.php und füge der Klasse zwei simple, aber unfassbar mächtige Eigenschaften hinzu:
1<?php
2
3namespace App\Jobs;
4
5use App\Models\Post;
6use Illuminate\Bus\Queueable;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Bus\Dispatchable;
9use Illuminate\Queue\InteractsWithQueue;
10use Illuminate\Queue\SerializesModels;
11
12class OptimizeArticleImages implements ShouldQueue
13{
14 use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
15
16 public $post;
17
18 // 1. Wir erlauben dem Worker, diesen Job bei einem Fehler bis zu 3 Mal zu versuchen!
19 public $tries = 3;
20
21 // 2. BACKOFF-STRATEGIE: Wenn der erste Versuch fehlschlägt, warte 60 Sekunden.
22 // Schlägt er wieder fehl, warte 120 Sekunden vor dem dritten und letzten Versuch.
23 public $backoff = [60, 120];
24
25 public function __construct(Post $post)
26 {
27 $this->post = $post;
28 }
29
30 public function handle(): void
31 {
32 // ... deine fehleranfällige Logik (z.B. API-Aufrufe, Bildverarbeitung)
33 }
34}Schau dir diese Architektur an. Das ist Software-Engineering auf allerhöchstem Niveau. Wenn die S3-API jetzt einen Timeout hat, stirbt dein Job nicht sofort. Der Worker sagt: "Okay, das hat nicht geklappt. Ich lege den Job zurück in die Warteschlange, aber mit einem Zeitstempel für in einer Minute." Der Worker bearbeitet in der Zwischenzeit andere Dinge. Nach 60 Sekunden probiert er es noch einmal. Erst wenn alle drei Versuche ($tries = 3) grandios gescheitert sind, wandert der Job endgültig in die failed_jobs MySQL-Tabelle.
Damit baut deine Applikation eine unglaubliche Resilienz (Widerstandsfähigkeit) gegen temporäre Netzwerkausfälle auf.

Die Königsdisziplin: Job Batches (Stapelverarbeitung)
Wir haben jetzt gelernt, wie wir einen einzelnen, schweren Job in den Hintergrund verlagern und ihn bei Fehlern elegant neu starten lassen. Für das Berechnen eines Titelbildes ist das absolut perfekt. Aber lass uns noch einmal zu unserem zweiten großen Beispiel zurückkehren: dem E-Mail-Newsletter.
Stell dir vor, dein Blog ist mittlerweile gigantisch. Du hast nicht 5.000, sondern 50.000 Abonnenten. Wenn du auf "Veröffentlichen" klickst, könntest du natürlich einfach eine foreach-Schleife in deinen Controller schreiben und 50.000 einzelne SendNewsletterJob-Pakete auf das rote Förderband werfen. Redis lacht darüber, das dauert nur wenige Sekunden. Deine Worker fangen danach sofort an, die Mails parallel zu verschicken.
Aber hier entsteht ein massives, logisches Architektur-Problem: Woher weißt du eigentlich, wann alle 50.000 E-Mails verschickt wurden?
Wenn dein Redakteur im Dashboard sitzt, möchte er vielleicht einen Fortschrittsbalken sehen. Oder er möchte eine Push-Benachrichtigung auf sein Smartphone bekommen, sobald die allerletzte E-Mail das Haus verlassen hat. Mit 50.000 isolierten, einzelnen Jobs auf dem Förderband hast du absolut keine Ahnung, wann das große Ganze abgeschlossen ist. Jeder Job kämpft für sich allein.
Genau für dieses Szenario hat Laravel eine der mächtigsten Funktionen des gesamten Frameworks entwickelt: Job Batches (Stapelverarbeitung).
Mit einem Batch schnüren wir tausende einzelne Jobs zu einem gigantischen Master-Paket zusammen. Wir behalten die extreme Geschwindigkeit der parallelen Verarbeitung durch unsere Worker, gewinnen aber die absolute Kontrolle über den Gesamtprozess zurück.
Den Master-Plan programmieren
Um Batches nutzen zu können, müssen wir unserer Datenbank zuerst erlauben, den Überblick zu behalten. Laravel braucht dafür eine spezielle Tabelle. Führe diese Befehle in deinem Terminal aus:
php artisan queue:batches-table
php artisan migrateJetzt öffnen wir unseren PostController und schreiben die Logik für unseren gigantischen Newsletter-Versand. Schau dir an, wie unglaublich lesbar und elegant dieser komplexe Prozess in Laravel aussieht:
1<?php
2
3namespace App\Http\Controllers\Admin;
4
5use App\Http\Controllers\Controller;
6use App\Models\Post;
7use App\Models\Subscriber;
8use App\Jobs\SendNewsletterJob;
9use Illuminate\Support\Facades\Bus;
10use Illuminate\Http\JsonResponse;
11
12class PostController extends Controller
13{
14 public function sendNewsletter(Post $post): JsonResponse
15 {
16 // Wir holen uns alle 50.000 Abonnenten aus der Datenbank
17 $subscribers = Subscriber::all();
18 $jobs = [];
19
20 // Wir erstellen für jeden Abonnenten einen eigenen, kleinen Job
21 foreach ($subscribers as $subscriber) {
22 $jobs[] = new SendNewsletterJob($post, $subscriber);
23 }
24
25 // BÄM! Die Magie der Job Batches.
26 // Wir übergeben das riesige Array an die Bus-Fassade.
27 $batch = Bus::batch($jobs)
28 ->then(function () use ($post) {
29 // HIER IST DER MAGISCHE CALLBACK!
30 // Dieser Code wird erst ausgeführt, wenn ALLE 50.000 Jobs zu 100% fertig sind!
31 $post->update(['newsletter_sent' => true]);
32
33 // Wir könnten hier auch eine Push-Benachrichtigung an den Admin senden
34 // oder den Redis-Cache über einen Observer leeren.
35 })
36 ->catch(function () {
37 // Dieser Code wird ausgeführt, sobald auch nur ein einziger Job
38 // endgültig (nach allen Retries) fehlschlägt.
39 \Log::error('Achtung: Der Newsletter-Batch hatte Fehler!');
40 })
41 ->name("Newsletter für Post ID: {$post->id}")
42 ->dispatch();
43
44 // Der Controller antwortet Next.js wieder in absoluter Rekordzeit!
45 return response()->json([
46 'message' => 'Der gigantische Newsletter-Batch wurde gestartet!',
47 'batch_id' => $batch->id // Mit dieser ID kann Next.js den Fortschritt abfragen!
48 ]);
49 }
50}Ist das nicht ein absolutes Meisterwerk der Softwarearchitektur? Du hast gerade 50.000 E-Mails in den Hintergrund verlagert. Deine Queue Worker stürzen sich wie ein Schwarm hungriger Piranhas auf diese Aufgaben. Sie arbeiten parallel, völlig losgelöst vom Nutzer.
Aber Laravel behält im Hintergrund pedantisch Buch. Es zählt jeden einzelnen erfolgreichen Job mit. Und in der exakten Millisekunde, in der Job Nummer 50.000 erfolgreich den Worker verlässt, feuert Laravel vollautomatisch die then()-Funktion ab. Wir setzen den Status in der Datenbank auf "gesendet" und der Kreis schließt sich perfekt.
Wenn du die batch_id an dein Next.js-Frontend zurückgibst, kannst du dort sogar eine Route bauen, die den prozentualen Fortschritt (z.B. "45% abgeschlossen") in Echtzeit abfragt und dem Redakteur einen fließenden Progress-Bar anzeigt. Das ist eine User Experience, die man sonst nur aus teuren Enterprise-SaaS-Produkten kennt!

Das Auge des Sturms: Laravel Horizon und Produktion
Wir haben in diesem dreizehnten Teil eine Architektur erschaffen, die selbst dem Ansturm eines Black-Friday-Sales standhalten würde. Wir haben schwere Aufgaben durch Laravel Background Jobs Queues komplett vom Next.js-Frontend entkoppelt. Wir haben gelernt, wie Worker diese Pakete im Hintergrund lautlos abarbeiten, wie wir bei Ausfällen intelligente Retries konfigurieren und wie wir mit Batches zehntausende Jobs zu einem kontrollierbaren Meisterwerk stapeln.
Aber wenn du dieses System jetzt live auf einen echten Server schiebst, wirst du schnell ein mulmiges Gefühl bekommen. Woher weißt du eigentlich, ob deine Worker gerade überlastet sind? Wie lange dauert die Verarbeitung eines einzelnen Bildes im Durchschnitt? Wie viele Jobs sind gerade in diesem Moment fehlgeschlagen?
Wenn du nur das nackte Terminal hast, fliegst du im Blindflug. Genau für dieses Problem hat das Laravel-Team ein exklusives, offizielles Paket entwickelt, das ausschließlich mit unserem geliebten Redis-Treiber funktioniert: Laravel Horizon.
Horizon ist ein wunderschönes, in Echtzeit aktualisiertes Dashboard für deine Kommandozentrale. Du installierst es mit zwei simplen Befehlen:
composer require laravel/horizon
php artisan horizon:installWenn du danach in deinem Browser deine-domain.de/horizon aufrufst, fällt dir die Kinnlade herunter. Du siehst sofort ein professionelles Interface. Du siehst deinen exakten Job-Durchsatz (Jobs pro Minute). Du kannst live zusehen, wie deine Batches (der Newsletter aus dem letzten Abschnitt) prozentual abgearbeitet werden. Du kannst mit einem Klick fehlgeschlagene Jobs analysieren und sie direkt über die Benutzeroberfläche neu starten, ohne jemals das Terminal berühren zu müssen.
Horizon macht aus einer abstrakten, unsichtbaren Hintergrund-Architektur ein messbares, greifbares Enterprise-System.
Der Wächter der Arbeiter: Supervisor
Ein letzter, absolut essenzieller Praxistipp für dein Deployment: Auf deinem lokalen MacBook oder Windows-Rechner hast du den Befehl php artisan queue:work (oder php artisan horizon) einfach in einem Terminal-Tab laufen lassen.
Wenn du das auf einem echten Linux-Produktionsserver machst und dann dein SSH-Fenster schließt, stirbt der Worker-Prozess sofort. Alle Jobs bleiben liegen.
In der echten Welt nutzt man für Hintergrundprozesse einen sogenannten Prozess-Monitor. Der absolute Industrie-Standard dafür heißt Supervisor. Du konfigurierst Supervisor auf deinem Linux-Server so, dass er Laravel Horizon permanent überwacht. Wenn Horizon aus irgendeinem Grund abstürzen sollte (z. B. weil der RAM voll ist), merkt Supervisor das in derselben Millisekunde und startet den Prozess sofort vollautomatisch neu. Dein System heilt sich selbst, während du schläfst. (Wenn du Laravel Forge für dein Server-Management nutzt, ist Supervisor bereits perfekt für dich vorkonfiguriert!).

Teil der Serie
Headless CMS mit Laravel und Next.js
Headless CMS mit Laravel 12 & Next.js: Der ultimative Guide Pillar
Das perfekte Laravel Next.js Setup: Projektstruktur für dein Headless CMS
Datenbank-Design für ein skalierbares Headless CMS
Perfekte RESTful API mit Laravel 12 & API Resources bauen
Sichere Laravel Sanctum Next.js Authentifizierung bauen
Das ultimative Next.js Admin Dashboard mit shadcn/ui bauen
Rollen & Rechte-Management mit Laravel Spatie integrieren
Next.js App Router Architektur für Admin & Public
CRUD im Frontend: React Hook Form & Zod mit API
Datei-Uploads und Media-Management über die API
React Server Components für maximalen SEO-Boost
Interaktives Kommentarsystem mit Next.js & Laravel
Skalierung auf Enterprise-Niveau: Laravel 12 Redis API Caching
Laravel Background Jobs Queues für extreme Geschwindigkeit
Laravel API absichern Rate Limiting & Security in der Praxis
CI/CD Deployment Laravel Next.js Plesk: Das große Finale
Häufig gestellte Fragen (FAQ)
Wie geht es weiter in Teil 14? Rate Limiting & Security: Die Laravel API absichern
Herzlichen Glückwunsch! Du hast nun ein Backend, das rohe Datenmengen cachen kann, SEO-freundlich ist und selbst schwerste Aufgaben asynchron in perfekten Warteschlangen abarbeitet. Dein System ist eine absolute Hochleistungsmaschine.
Aber genau hier lauert die nächste, unsichtbare Gefahr im freien Netz. Eine API, die so unfassbar schnell und effizient antwortet, ist ein gefundenes Fressen für automatisierte Skripte, bösartige Bots und Scraping-Tools.
Was passiert eigentlich, wenn ein Angreifer oder ein verrückt gewordenes Skript beschließt, deinen Endpunkt für das Posten von Kommentaren (oder das Auslösen des Newsletters) mit 10.000 Anfragen pro Sekunde zu bombardieren? Deine Queue würde überlaufen, dein Redis-Speicher würde an sein Limit geraten und dein Server würde unter der schieren Masse an böswilligem Traffic kapitulieren. All unsere harte Arbeit an der Performance würde gegen uns verwendet werden.
Wir müssen unsere wertvolle Infrastruktur verteidigen. Im kommenden Teil 14 stürzen wir uns auf das kritische Thema: Rate Limiting & Security: Die Laravel API absichern.
Wir werden Laravel beibringen, wie ein gnadenloser, intelligenter Türsteher zu agieren. Ich zeige dir, wie wir mit der mächtigen Rate Limiting Fassade von Laravel IP-Adressen dynamisch drosseln (Throttling), sobald sie zu viele Anfragen in zu kurzer Zeit senden. Der Angreifer bekommt dann nur noch einen kühlen 429 Too Many Requests Fehler, während unsere echten Nutzer völlig ungestört weiter surfen.
Außerdem werfen wir einen tiefen Blick auf essentielle HTTP-Security-Header, härten unsere CORS-Konfiguration exklusiv für unser Next.js-Frontend ab und stellen sicher, dass unsere Endpunkte kugelsicher sind.
Ruh dich aus. Feiere deine heutige asynchrone Architektur. Sag mir einfach Bescheid, wenn du bereit bist, die virtuellen Schutzschilde hochzufahren und Teil 14 zu starten!

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


