Long-Running Operations mit 202 Accepted und Polling modellieren
AI generated
{ }
GET
Async-Pattern · HTTP 202
Long-Running Operations mit 202 Accepted
Wie langlaufende Operationen REST-konform modelliert werden, ohne den Client ewig warten zu lassen

Ein Videotranscoding, ein großer Datenexport oder eine komplexe Report-Generierung passt selten in die Antwortzeit eines einzelnen synchronen HTTP-Requests. Das Long-Running-Operations-Pattern löst dieses Problem, indem die Operation sofort mit HTTP 202 Accepted bestätigt wird, während die eigentliche Arbeit asynchron im Hintergrund läuft und der Client den Fortschritt über einen separaten Status-Endpoint abfragt.

15 Min. Lesezeit 202 Accepted · Async-Pattern Symfony Messenger

1. Warum synchrone Requests bei langen Operationen scheitern

HTTP-Requests haben in der Praxis harte Zeitlimits, gesetzt durch Load Balancer, Reverse Proxies, Browser und den PHP-FPM-Prozess selbst, typischerweise zwischen 30 und 120 Sekunden. Eine Operation, die mehrere Minuten oder gar Stunden benötigt (ein umfangreicher Datenexport, eine Batch-Bildverarbeitung, eine komplexe Report-Berechnung über Millionen Datensätze), überschreitet diese Limits zuverlässig, unabhängig davon, wie gut der Server-Code optimiert ist.

Ein naiver Versuch, das Timeout einfach zu erhöhen, verschiebt das Problem nur, statt es zu lösen, und macht die Anwendung anfällig für aufgehängte Worker-Prozesse, die während der gesamten Operationsdauer blockiert bleiben. Das Long-Running-Operations-Pattern löst das Problem strukturell, indem es die HTTP-Antwortzeit von der tatsächlichen Verarbeitungszeit entkoppelt.

Ein zusätzlicher, oft unterschätzter Vorteil dieser Entkopplung ist, dass der HTTP-Server-Prozess selbst nach Bestätigung der Anfrage sofort wieder für andere Requests frei wird, statt für die gesamte Dauer der Operation blockiert zu bleiben, was die Gesamtkapazität der Anwendung unter Last spürbar erhöht.

2. HTTP 202 Accepted als sofortige Bestätigung

HTTP 202 Accepted signalisiert, dass eine Anfrage zur Verarbeitung angenommen wurde, die Verarbeitung selbst aber noch nicht abgeschlossen ist, im Unterschied zu HTTP 200 OK oder HTTP 201 Created, die eine bereits abgeschlossene Operation signalisieren. Die Antwort auf einen Long-Running-Operation-Trigger sollte deshalb sofort mit 202 antworten, typischerweise binnen Millisekunden, verbunden mit einer Job-ID und einem Location-Header, der auf den zugehörigen Status-Endpoint verweist.

Dieser Statuscode ist Teil des HTTP-Standards selbst und wird von REST-Clients und Frameworks korrekt als "angenommen, aber nicht fertig" interpretiert, im Gegensatz zu einem missbräuchlich verwendeten 200 mit einem selbst erfundenen Status-Feld im Body, das viele generische HTTP-Tools nicht automatisch erkennen.


<?php
declare(strict_types=1);

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Messenger\MessageBusInterface;

final class ReportGenerationController
{
    public function __construct(private readonly MessageBusInterface $bus) {}

    public function trigger(array $reportParams): JsonResponse
    {
        $jobId = bin2hex(random_bytes(16));
        $this->bus->dispatch(new GenerateReportMessage($jobId, $reportParams));

        return new JsonResponse(
            ['jobId' => $jobId, 'status' => 'pending'],
            202,
            ['Location' => "/api/reports/jobs/{$jobId}"]
        );
    }
}

3. Der Status-Endpoint für Polling

Der über den Location-Header referenzierte Status-Endpoint sollte mindestens den aktuellen Status (pending, processing, completed, failed) zurückgeben, idealerweise ergänzt um einen numerischen Fortschritt und eine geschätzte Restdauer, sofern diese sinnvoll berechenbar ist. Solange der Job noch läuft, antwortet der Endpoint mit HTTP 200 und dem aktuellen Status, nicht mit 202, weil die Statusabfrage selbst sofort abgeschlossen ist, auch wenn der referenzierte Job noch nicht fertig ist.

Ist der Job abgeschlossen, sollte der Status-Endpoint entweder das fertige Ergebnis direkt enthalten (bei kleineren Ergebnissen) oder einen Link zum Ergebnis liefern (bei größeren Ergebnissen wie generierten Dateien), zusammen mit einem klaren completed-Status, damit der Client eindeutig erkennt, dass weiteres Polling nicht mehr nötig ist.

4. Sinnvolle Polling-Intervalle statt aggressivem Dauerabfragen

Ein Client, der den Status-Endpoint alle 100 Millisekunden abfragt, erzeugt unnötige Last, während ein zu seltenes Polling (etwa alle 30 Sekunden) die wahrgenommene Reaktionsfähigkeit für kurze Jobs unnötig verschlechtert. Ein bewährtes Muster ist exponentiell steigendes Polling, beginnend bei kurzen Intervallen (etwa 1 Sekunde) und ansteigend bis zu einem Maximalintervall (etwa 10 Sekunden), um kurze Jobs schnell zu erkennen, ohne bei langen Jobs unnötig Last zu erzeugen.

Der Status-Endpoint kann diesen Prozess zusätzlich unterstützen, indem er über einen Retry-After-Header dem Client explizit mitteilt, wie lange er vor der nächsten Abfrage warten soll, statt den Client mit einer festen, hartcodierten Client-Logik allein zu lassen. Dieser Header ist Teil des HTTP-Standards und wird von vielen HTTP-Client-Bibliotheken automatisch respektiert.

5. Webhooks als Alternative zu reinem Polling

Für Clients, die eine eigene öffentlich erreichbare URL betreiben können, ist ein Webhook-Callback oft effizienter als Polling: Statt wiederholt nachzufragen, registriert der Client bei der ursprünglichen Anfrage eine Callback-URL, die der Server bei Abschluss des Jobs mit dem Ergebnis aufruft. Dieser Ansatz reduziert unnötigen Traffic drastisch, besonders bei Jobs mit stark variabler, schwer vorhersagbarer Laufzeit.

Webhooks bringen eigene Komplexität mit sich, etwa Retry-Logik bei fehlgeschlagener Zustellung, Signatur-Verifikation gegen gefälschte Callbacks und die Notwendigkeit, dass der Client öffentlich erreichbar ist, weshalb viele APIs beide Mechanismen parallel anbieten: Polling als einfache Standardoption, Webhooks als effizientere Option für Clients, die den zusätzlichen Aufwand rechtfertigen können.

6. Fehlerbehandlung bei fehlgeschlagenen Long-Running-Operations

Schlägt ein Job während der asynchronen Verarbeitung fehl, sollte der Status-Endpoint einen expliziten failed-Status mit einer strukturierten Fehlerbeschreibung liefern, statt den Job unbegrenzt im pending-Status zu belassen, was den Client zu endlosem, sinnlosem Polling zwingen würde. Ein Timeout auf Job-Ebene, der einen Job nach einer maximalen Laufzeit automatisch als failed markiert, verhindert zusätzlich, dass hängende Hintergrundprozesse unentdeckt bleiben.

Für Jobs, die teilweise erfolgreich waren (etwa ein Datenexport, der bei 80 Prozent abbricht), sollte der Status-Endpoint dies explizit als eigenen Zustand kommunizieren, verbunden mit Informationen darüber, welcher Teil bereits abgeschlossen wurde, statt den gesamten Job pauschal als failed zu markieren. Diese differenzierte Fehlerkommunikation gibt dem Client die Möglichkeit, gezielt nur den fehlgeschlagenen Teil erneut anzustoßen, statt den kompletten Job von vorne zu starten.

7. Wie lange Job-Status-Informationen aufbewahrt werden sollten

Job-Status-Informationen sollten nicht unbegrenzt aufbewahrt werden, sondern nach einer angemessenen Frist (etwa 7 bis 30 Tage, abhängig vom Anwendungsfall) automatisch bereinigt werden, um den Speicherbedarf des Status-Speichers (oft Redis oder eine dedizierte Datenbanktabelle) nicht unkontrolliert wachsen zu lassen. Ein Abruf eines abgelaufenen Job-Status sollte mit HTTP 404 Not Found antworten, nicht mit einem irreführenden pending-Status.

Diese Aufbewahrungsfrist sollte klar in der API-Dokumentation kommuniziert werden, damit Clients wissen, wie lange sie sich auf die Verfügbarkeit eines Job-Ergebnisses verlassen können, bevor sie es selbst dauerhaft speichern müssen. Ein automatisierter Cleanup-Job, der abgelaufene Einträge periodisch entfernt, verhindert zusätzlich, dass die Aufbewahrungsfrist nur auf dem Papier existiert, aber in der Praxis nie durchgesetzt wird, was insbesondere bei einer hohen Anzahl täglich erzeugter Jobs schnell zu einem spürbaren Speicherproblem werden kann.

8. Laufende Operationen gezielt abbrechbar machen

Ein oft übersehener, aber praktisch wichtiger Aspekt ist die Möglichkeit, einen laufenden Job explizit abzubrechen, etwa wenn ein Nutzer einen versehentlich gestarteten, ressourcenintensiven Export stoppen möchte, statt ihn bis zum Ende laufen zu lassen und das Ergebnis danach zu verwerfen. Ein DELETE auf den Job-Status-Endpoint ist eine naheliegende, REST-konforme Möglichkeit, einen Abbruch anzufordern.

Wichtig ist, dass ein Abbruch-Request selbst asynchron behandelt wird: Der Server bestätigt die Abbruchanfrage sofort, der eigentliche Hintergrundprozess muss aber selbst regelmäßig prüfen, ob ein Abbruch angefordert wurde (etwa über ein Flag im Job-Status-Speicher), da eine sofortige, harte Unterbrechung eines laufenden Prozesses inkonsistente Zwischenzustände hinterlassen kann. Der Job-Status sollte deshalb einen eigenen cancelling-Zustand kennen, bevor der endgültige cancelled-Status tatsächlich erreicht wird.

9. Long-Running Operations im Überblick

Die folgende Tabelle vergleicht Polling und Webhooks als Kommunikationsmechanismen.

Mechanismus Vorteil Nachteil
Polling Einfach zu implementieren, keine öffentliche Client-URL nötig Unnötiger Traffic bei falscher Intervall-Wahl
Webhooks Effizient, sofortige Benachrichtigung bei Abschluss Client braucht öffentlich erreichbare URL
202 Accepted Sofortige Bestätigung, klare HTTP-Semantik Erfordert zusätzlichen Status-Endpoint
Retry-After-Header Steuert Polling-Intervall serverseitig Wird nicht von allen Clients automatisch respektiert

Mironsoft

OpenAPI-Design, Symfony-APIs und API-Sicherheit

APIs, die externe Teams ohne Rückfragen integrieren können?

Wir prüfen bestehende REST-APIs auf inkonsistente Fehlerformate, fehlende OpenAPI-Dokumentation und Sicherheitslücken und bauen daraus eine API, die klar dokumentiert, versioniert und gegen Missbrauch abgesichert ist.

API-Review

OpenAPI-Spezifikation, Fehlerformate und Statuscodes auf Konsistenz prüfen.

Symfony-Umsetzung

DTOs, Serializer und Validator für saubere, typsichere Request/Response-Modelle einsetzen.

Security-Audit

Rate-Limiting, Auth-Schemes und Input-Validierung gegen echte Angriffsflächen absichern.

10. Zusammenfassung

Long-Running Operations: Das Wichtigste auf einen Blick

202 Accepted

Signalisiert sofortige Annahme einer Operation, die noch nicht abgeschlossen ist, mit Job-ID und Location-Header.

Status-Endpoint

Liefert aktuellen Status, Fortschritt und bei Abschluss das Ergebnis oder einen Link darauf.

Polling-Intervalle

Exponentiell steigend statt fest, idealerweise gesteuert über den Retry-After-Header.

Webhooks als Alternative

Effizienter als Polling für Clients mit öffentlich erreichbarer Callback-URL.

11. FAQ: Long-Running Operations: Das Wichtigste auf einen Blick

1Ab welcher erwarteten Laufzeit lohnt sich dieses Pattern?
Ab etwa 5 bis 10 Sekunden wird es relevant, spätestens deutlich vor den typischen 30 bis 120 Sekunden Timeout-Limits von Load Balancern.
2Muss ich Symfony Messenger für die asynchrone Verarbeitung nutzen?
Nein, jede Message-Queue-Lösung funktioniert. Symfony Messenger ist im Symfony-Ökosystem die naheliegende, gut integrierte Wahl.
3Wie berechne ich eine sinnvolle Restdauer-Schätzung?
Basierend auf historischen Durchschnittswerten ähnlicher, bereits abgeschlossener Jobs, nicht auf einer statischen Schätzung, die die tatsächliche Datenmenge ignoriert.
4Sollte der Job-ID vorhersehbar oder zufällig sein?
Zufällig und ausreichend lang, um Erraten oder Aufzählen fremder Job-IDs durch andere Nutzer zu verhindern.
5Kann ich Long-Running Operations mit Bulk-Endpoints kombinieren?
Ja, sehr große Bulk-Batches sind ein häufiger, praktischer Anwendungsfall für genau dieses Pattern.
6Wie teste ich Long-Running Operations automatisiert?
Mit Integrationstests, die den synchronen Message-Bus-Transport für Tests nutzen, um asynchrone Verarbeitung deterministisch und ohne echte Wartezeit zu simulieren.
7Was passiert, wenn der Client nie den Status abfragt?
Der Job läuft trotzdem im Hintergrund weiter, das Ergebnis wird bis zum Ablauf der Aufbewahrungsfrist gespeichert, auch ohne dass es je abgefragt wird.
8Ist WebSocket eine bessere Alternative zu Polling?
Für Echtzeit-Fortschrittsanzeigen ja, siehe Server-Sent Events vs. WebSockets für API-Echtzeit-Updates. Für einfache Abschluss-Benachrichtigung sind Webhooks oft ausreichend.
9Wie sichere ich den Status-Endpoint gegen fremden Zugriff ab?
Über dieselbe Authentifizierung wie den ursprünglichen Trigger-Endpoint, kombiniert mit einer Prüfung, dass der abfragende Nutzer auch der Ersteller des Jobs ist.
10Sollte 202 Accepted auch für schnelle Operationen genutzt werden?
Nein, für Operationen, die typischerweise unter einer Sekunde abschließen, ist eine direkte synchrone Antwort mit 200 oder 201 einfacher und für Clients naheliegender.