Wie ein Bulk-Endpoint mit partiellen Fehlern umgeht, ohne Clients zu verwirren
Wenn ein Client hunderte Entitäten anlegen oder aktualisieren muss, sind hunderte einzelne HTTP-Requests ineffizient, sowohl für den Netzwerk-Overhead als auch für die Serverlast. Ein gut designter Bulk-Endpoint reduziert diesen Overhead drastisch, bringt aber eine eigene Design-Herausforderung mit sich: Wie wird kommuniziert, wenn einige Elemente eines Batches erfolgreich sind und andere fehlschlagen?
Inhaltsverzeichnis
- 1. Warum einzelne Requests bei großen Datenmengen nicht skalieren
- 2. Response-Format für gemischte Erfolge und Fehler
- 3. Atomare Transaktion vs. Best-Effort-Verarbeitung
- 4. Größenlimits für Batches sinnvoll setzen
- 5. Idempotenz bei wiederholten Bulk-Requests
- 6. Asynchrone Verarbeitung für sehr große Batches
- 7. Umgang mit Reihenfolge und Abhängigkeiten zwischen Batch-Elementen
- 8. Bulk-Endpoints separat von Einzel-Endpoints überwachen
- 9. Bulk-Operationen im Überblick
- 10. Zusammenfassung
- 11. FAQ
1. Warum einzelne Requests bei großen Datenmengen nicht skalieren
Jeder einzelne HTTP-Request bringt einen festen Overhead mit sich, TCP-Handshake (sofern keine Connection wiederverwendet wird), TLS-Handshake, HTTP-Header-Parsing und Routing, der bei tausend Einzelrequests tausendfach anfällt, während er bei einem einzigen Bulk-Request nur einmal entsteht. Für Anwendungsfälle wie einen initialen Datenimport, eine Synchronisation zwischen Systemen oder ein CSV-Upload mit tausenden Zeilen macht dieser Unterschied den Unterschied zwischen einer Operation, die Sekunden dauert, und einer, die Minuten oder Stunden benötigt.
Zusätzlich zum reinen Netzwerk-Overhead ermöglicht ein Bulk-Endpoint dem Server, Datenbankoperationen zu bündeln (etwa einen einzigen Bulk-INSERT statt tausend Einzel-INSERTs), was die tatsächliche Verarbeitungszeit auf Serverseite ebenfalls drastisch reduziert, verglichen mit tausend separaten Transaktionen.
Für Clients bedeutet ein Bulk-Endpoint zudem deutlich einfacheren Fehlerbehandlungscode: Statt tausend einzelne Try-Catch-Blöcke und tausend potenzielle Netzwerkfehler zu behandeln, muss der Client nur eine einzige Anfrage überwachen und deren strukturiertes Ergebnis auswerten, was den Integrationsaufwand auf Client-Seite spürbar reduziert.
2. Response-Format für gemischte Erfolge und Fehler
Die zentrale Design-Frage bei Bulk-Endpoints ist, wie die Antwort aussieht, wenn von zehn übermittelten Elementen acht erfolgreich verarbeitet wurden und zwei aus fachlichen Gründen fehlschlugen (etwa eine Validierungsregel oder ein Duplikat). Ein einzelner globaler HTTP-Statuscode kann diese gemischte Situation nicht abbilden, weshalb die Antwort ein Array mit einem Ergebnis pro Element enthalten sollte, jedes mit eigenem Status, eigener ID-Referenz zum ursprünglichen Element und gegebenenfalls eigener Fehlermeldung.
HTTP 207 Multi-Status, ursprünglich aus WebDAV stammend, wird von manchen APIs für genau diesen Fall verwendet, ist aber nicht universell etabliert. Ein pragmatischer Alternativansatz ist HTTP 200 mit einem strukturierten Body, der explizit zwischen erfolgreichen und fehlgeschlagenen Elementen unterscheidet, solange diese Konvention klar dokumentiert ist.
Unabhängig von der gewählten Konvention sollte jedes Element im Ergebnis-Array eindeutig auf sein ursprüngliches Element im Request zurückführbar sein, entweder über den Index in der ursprünglichen Liste oder über eine client-generierte ID, damit der Client Fehler gezielt dem richtigen Datensatz zuordnen kann.
<?php
declare(strict_types=1);
final class BulkOrderCreateController
{
public function bulkCreate(array $orderPayloads): array
{
$results = [];
foreach ($orderPayloads as $index => $payload) {
try {
$order = $this->orderService->create($payload);
$results[] = [
'index' => $index,
'status' => 'success',
'id' => $order->getId(),
];
} catch (ValidationException $e) {
$results[] = [
'index' => $index,
'status' => 'error',
'error' => $e->getMessage(),
];
}
}
return [
'total' => count($orderPayloads),
'succeeded' => count(array_filter($results, fn ($r) => $r['status'] === 'success')),
'failed' => count(array_filter($results, fn ($r) => $r['status'] === 'error')),
'results' => $results,
];
}
}
3. Atomare Transaktion vs. Best-Effort-Verarbeitung
Ein Bulk-Endpoint muss explizit entscheiden, ob ein Batch als atomare Einheit behandelt wird (entweder alle Elemente erfolgreich oder komplettes Rollback) oder im Best-Effort-Modus arbeitet (jedes Element wird unabhängig verarbeitet, einzelne Fehler blockieren die übrigen Elemente nicht). Beide Ansätze haben legitime Anwendungsfälle, aber sehr unterschiedliches Verhalten, weshalb die Wahl explizit kommuniziert und idealerweise über einen Query- oder Body-Parameter steuerbar sein sollte.
Atomare Semantik passt gut zu finanziell sensiblen Batches, bei denen ein teilweiser Erfolg inakzeptable Inkonsistenzen erzeugen würde (etwa bei einer Reihe zusammenhängender Buchungen). Best-Effort-Semantik passt besser zu unabhängigen Elementen, bei denen ein einzelner Fehler nicht die Verarbeitung der übrigen, korrekten Elemente verhindern soll, etwa bei einem Produktkatalog-Import mit teilweise fehlerhaften Datensätzen.
4. Größenlimits für Batches sinnvoll setzen
Ein Bulk-Endpoint ohne Größenlimit lädt dazu ein, dass ein Client versucht, hunderttausende Elemente in einem einzigen Request zu übermitteln, was sowohl den Speicherverbrauch auf Serverseite als auch die Anfragedauer unkontrollierbar macht und im schlimmsten Fall zu einem Timeout führt, ohne dass der Client weiß, ob überhaupt etwas verarbeitet wurde. Ein explizites Limit (etwa maximal 500 Elemente pro Request) mit einer klaren Fehlermeldung bei Überschreitung ist deshalb Pflicht, nicht optional.
Für größere Datenmengen als das Limit erlaubt, sollte die API-Dokumentation explizit auf Pagination des Uploads (mehrere Bulk-Requests nacheinander) oder auf einen asynchronen Bulk-Import-Mechanismus mit Job-Status-Polling verweisen, statt Clients ohne Anleitung mit der Fehlermeldung allein zu lassen.
5. Idempotenz bei wiederholten Bulk-Requests
Bei einem Netzwerkfehler mitten in einem Bulk-Request weiß der Client oft nicht sicher, ob der Server den Batch bereits (teilweise) verarbeitet hat, bevor die Antwort verloren ging. Ohne Idempotenz-Schutz führt ein naiver Retry des kompletten Batches zu doppelt angelegten Datensätzen für die bereits erfolgreich verarbeiteten Elemente.
Ein Idempotency-Key pro Batch-Request, analog zum Einzel-Request-Muster, verhindert dieses Problem: Der Server erkennt einen wiederholten Request mit demselben Key und liefert die zwischengespeicherte ursprüngliche Antwort erneut aus, statt den Batch ein zweites Mal zu verarbeiten. Zusätzlich kann jedes Element im Batch eine eigene, client-generierte ID tragen, um serverseitige Duplikatserkennung auf Element-Ebene zu ermöglichen, was besonders bei Best-Effort-Verarbeitung mit teilweise erfolgreichen Batches sehr wichtig und praktisch relevant ist.
6. Asynchrone Verarbeitung für sehr große Batches
Für Batches, die die synchrone Verarbeitungszeit eines einzelnen HTTP-Requests sprengen würden (typischerweise mehr als wenige Sekunden), sollte der Bulk-Endpoint stattdessen HTTP 202 Accepted mit einer Job-ID zurückgeben und die tatsächliche Verarbeitung asynchron über eine Message-Queue durchführen, während der Client den Fortschritt über einen separaten Status-Endpoint abfragt. Dieses Muster verbindet Bulk-Operationen mit dem Long-Running-Operations-Pattern.
Der Status-Endpoint sollte dabei nicht nur einen globalen Fortschritt (etwa 340 von 1000 verarbeitet) zurückgeben, sondern idealerweise auch bereits abgeschlossene Teilresultate, damit ein Client frühzeitig auf einzelne Fehler reagieren kann, statt auf das Ende der gesamten Verarbeitung warten zu müssen.
7. Umgang mit Reihenfolge und Abhängigkeiten zwischen Batch-Elementen
Manche Batches enthalten Elemente mit Abhängigkeiten untereinander, etwa wenn ein Element im selben Batch auf die client-generierte ID eines anderen Elements verweist (ein Bestellposten, der auf eine im selben Batch neu angelegte Produktvariante verweist). Der Server muss in diesem Fall entweder eine Verarbeitungsreihenfolge garantieren, die solche Abhängigkeiten respektiert, oder klar dokumentieren, dass Elemente unabhängig und in beliebiger Reihenfolge verarbeitet werden, sodass Clients Abhängigkeiten selbst über mehrere sequenzielle Batches auflösen müssen.
Eine explizite Dokumentation dieser Garantie (oder ihres Fehlens) ist wichtig, weil Clients sonst implizit von einer Reihenfolge ausgehen könnten, die der Server tatsächlich nicht garantiert, was zu schwer reproduzierbaren, race-artigen Fehlern führen kann, die erst unter Produktionslast sichtbar werden und in Testumgebungen mit geringerer Parallelität kaum auffallen.
8. Bulk-Endpoints separat von Einzel-Endpoints überwachen
Bulk-Endpoints haben ein grundlegend anderes Lastprofil als Einzel-Endpoints: Ein einzelner Bulk-Request kann intern hunderte Datenbankoperationen auslösen, weshalb Standard-Metriken wie Requests pro Sekunde allein irreführend sind, wenn sie Bulk- und Einzel-Endpoints ungetrennt zusammenfassen. Aussagekräftiger sind Metriken wie verarbeitete Elemente pro Sekunde und durchschnittliche Batch-Größe, getrennt erfasst für Bulk-Endpoints.
Ebenso wichtig ist eine getrennte Fehlerrate pro Element statt nur pro Request, da ein einzelner fehlgeschlagener Bulk-Request mit 500 Elementen davon 495 erfolgreich verarbeiten könnte, was in einer reinen Request-Fehlerrate als vollständiger Misserfolg erscheinen würde, obwohl die tatsächliche Erfolgsquote bei 99 Prozent liegt. Ein Dashboard, das beide Sichten nebeneinander zeigt, verhindert diese Fehlinterpretation und macht sichtbar, ob Probleme auf einzelne fehlerhafte Datensätze oder auf systematische Ausfälle zurückzuführen sind.
9. Bulk-Operationen im Überblick
Die folgende Tabelle vergleicht die wichtigsten Design-Entscheidungen für Bulk-Endpoints.
| Aspekt | Option A | Option B |
|---|---|---|
| Fehlerbehandlung | Atomar: alles oder nichts | Best-Effort: unabhängige Elemente |
| Verarbeitung | Synchron mit direkter Antwort | Asynchron mit Job-ID und Status-Polling |
| Antwortformat | HTTP 207 Multi-Status | HTTP 200 mit strukturiertem Ergebnis-Array |
| Größenlimit | Hart begrenzt (z.B. 500 Elemente) | Unbegrenzt mit asynchroner Verarbeitung |
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
Bulk-Operationen: Das Wichtigste auf einen Blick
Warum Bulk
Reduziert Netzwerk-Overhead und ermöglicht gebündelte Datenbankoperationen gegenüber tausenden Einzelrequests.
Partial Failure
Antwort braucht ein Ergebnis pro Element, da ein einzelner globaler Statuscode gemischte Erfolge nicht abbilden kann.
Atomar vs. Best-Effort
Muss explizit gewählt und kommuniziert werden, beide Modi haben legitime, unterschiedliche Anwendungsfälle.
Idempotenz
Idempotency-Key pro Batch verhindert doppelte Verarbeitung bei Netzwerkfehlern und Retries.