wann sie synchrone Calls in Magento 2 ablösen sollten
Synchrone REST-Calls blockieren PHP-FPM-Worker und laufen bei Massendaten in HTTP-Timeouts. Die asynchrone Bulk-API von Magento 2 verlagert Verarbeitung in die Message Queue und macht Massenimporte, Preisupdates und ERP-Anbindungen skalierbar, ohne den Checkout-Thread zu blockieren.
Inhaltsverzeichnis
- 1. Warum synchrone REST-Calls bei Massendaten an ihre Grenzen stoßen
- 2. Architektur der asynchronen Web-API in Magento
- 3. Bulk-API im Detail
- 4. queue.xml und communication.xml konfigurieren
- 5. Eigene asynchrone Endpoints und Consumer bauen
- 6. Operation-Status abfragen und überwachen
- 7. RabbitMQ vs. MySQL als Message Broker
- 8. Fehlerbehandlung, Retries und Idempotenz bei Bulk-Operationen
- 9. Praxisbeispiel und Vergleich: Massen-Preisupdate
- 10. Zusammenfassung
- 11. FAQ
1. Warum synchrone REST-Calls bei Massendaten an ihre Grenzen stoßen
Ein synchroner REST-Call in Magento 2 belegt für die gesamte Dauer der Anfrage einen PHP-FPM-Worker-Prozess. Bei einem einzelnen Produkt-Update ist das unproblematisch, weil die Anfrage innerhalb weniger Millisekunden abgeschlossen ist. Sobald aber ein ERP- oder PIM-System 5.000 oder 10.000 SKUs in einer Schleife über einzelne PUT /rest/V1/products/:sku-Aufrufe aktualisiert, summiert sich die Blockierzeit auf mehrere Minuten pro Import-Lauf, und jeder dieser Aufrufe hält währenddessen einen kompletten Worker fest. Ist der FPM-Pool mit pm.max_children begrenzt, was in jeder produktiven Installation der Fall ist, stehen für andere gleichzeitige Anfragen, etwa Checkout-Requests echter Kunden, entsprechend weniger Worker zur Verfügung.
Nginx und PHP-FPM haben in der Standardkonfiguration Timeout-Werte, die für Einzel-Requests gedacht sind: fastcgi_read_timeout liegt oft bei 60 Sekunden, max_execution_time in php.ini häufig ebenfalls im niedrigen zweistelligen Bereich. Ein synchroner Bulk-Import, der intern hunderte Datenbank-Schreibvorgänge, Indexer-Trigger und Event-Observer auslöst, überschreitet diese Grenzen zuverlässig. Das Resultat ist ein abgebrochener HTTP-Request, dessen Server-Prozess aber unter Umständen weiterläuft und Daten inkonsistent hinterlässt, weil der Client den Abbruch nicht sauber vom tatsächlichen Verarbeitungsstand unterscheiden kann.
Noch gravierender ist der Effekt auf den laufenden Betrieb: Während ein Massenimport synchrone Worker blockiert, kann es zu spürbaren Verzögerungen bei parallelen Checkout-Vorgängen kommen, weil derselbe FPM-Pool beide Lasten bedient. Genau an diesem Punkt setzt die asynchrone Bulk-API an: Sie entkoppelt die Annahme einer Anfrage von ihrer tatsächlichen Verarbeitung, gibt den HTTP-Worker sofort wieder frei und verlagert die eigentliche Arbeit in einen Consumer-Prozess außerhalb des Web-Request-Zyklus.
2. Architektur der asynchronen Web-API in Magento
Magento 2 stellt für praktisch jeden bestehenden REST-Endpoint automatisch eine asynchrone Variante bereit, ohne dass dafür zusätzlicher Code im jeweiligen Modul nötig ist. Der Schlüssel dazu ist der HTTP-Header X-Asynchronous: true. Wird ein regulärer Request wie POST /rest/V1/products mit diesem Header gesendet, greift das Framework über den AsynchronousSchemaBuilder und die zugehörige Webapi-Rewrite-Logik ein, bevor der eigentliche Service-Contract aufgerufen wird. Statt das Ergebnis der Operation synchron zurückzugeben, wird die Anfrage in eine Message-Queue-Nachricht umgewandelt, in der Datenbanktabelle magento_bulk beziehungsweise magento_operation referenziert und der Client erhält sofort eine Antwort mit HTTP-Status 202 (Accepted) sowie einer bulk_uuid, unter der der Fortschritt später abgefragt werden kann.
Technisch basiert dieser Mechanismus auf der generischen Konsument-Pipeline async.operation.add, die für jeden Service-Contract eine passende asynchrone Route über webapi.xml-Deklarationen registriert. Wichtig ist: Es muss keine eigene async-Route in webapi.xml definiert werden, das Framework generiert die asynchrone Variante automatisch aus der synchronen Deklaration, solange der zugrunde liegende Service-Contract über ein *Interface::save() oder vergleichbares Repository-Pattern verfügt. Der folgende Ausschnitt zeigt eine reguläre synchrone Route, wie sie in nahezu jedem Magento-Modul vorkommt, aus der das Framework die asynchrone Variante ableitet.
<?xml version="1.0"?>
<!-- app/code/Mironsoft/PriceSync/etc/webapi.xml -->
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-webapi:etc/webapi.xsd">
<!-- Standard synchronous route -->
<route url="/V1/mironsoft-pricesync/prices" method="POST">
<service class="Mironsoft\PriceSync\Api\PriceUpdateRepositoryInterface" method="save"/>
<resources>
<resource ref="Magento_Catalog::catalog"/>
</resources>
</route>
<!-- Called with header "X-Asynchronous: true" against the SAME url;
Magento generates the bulk-capable variant automatically -->
</routes>
Der entscheidende Unterschied zwischen einem gewöhnlichen synchronen Call und dem asynchronen Pendant liegt also nicht in der URL oder in zusätzlichem XML, sondern ausschließlich im Anfrage-Header und im Antwortverhalten. Das macht die asynchrone Web-API für bestehende Integrationen besonders attraktiv: Ein Client, der bereits gegen /rest/V1/products spricht, kann durch das einfache Hinzufügen eines Headers auf asynchrone Verarbeitung umstellen, ohne den Endpoint zu wechseln oder Request-Payloads anzupassen.
3. Bulk-API im Detail
Während die asynchrone Web-API einen einzelnen Request in eine einzelne Queue-Nachricht überführt, geht die Bulk-API einen Schritt weiter: Sie nimmt ein Array von Items in einem einzigen HTTP-Request entgegen und zerlegt es serverseitig in mehrere OperationInterface-Instanzen, die alle dieselbe bulk_uuid teilen. Diese Aufteilung übernimmt BulkManagementInterface::scheduleBulk(), das aus \Magento\AsynchronousOperations\Api zentral für alle Bulk-fähigen Endpoints zuständig ist. Jede einzelne Operation trägt zusätzlich eine eigene operation_key, mit der sich das Ergebnis eines konkreten Items innerhalb des Bulk-Vorgangs später eindeutig zuordnen lässt.
Der praktische Unterschied zeigt sich am deutlichsten bei Massenoperationen: Sendet ein Client 500 Produktänderungen an einen asynchronen Einzel-Endpoint, entstehen 500 separate HTTP-Requests mit jeweils eigener bulk_uuid und jeweils eigenem Netzwerk-Overhead durch TLS-Handshake, HTTP-Header und Token-Validierung. Sendet derselbe Client dieselben 500 Änderungen an einen Bulk-fähigen Endpoint wie POST /rest/V1/products/bySku mit einem Array im Body, entsteht ein einziger HTTP-Request mit einer gemeinsamen bulk_uuid und einer Liste von Operation-IDs in der Antwort. Die Verarbeitung selbst läuft weiterhin über einzelne Consumer-Aufrufe pro Operation, aber der Netzwerk- und Authentifizierungs-Overhead entfällt fast vollständig.
Intern implementiert OperationInterface Felder wie bulk_uuid, topic_name, serialized_data und status, die in der Tabelle magento_operation persistiert werden. Jede Operation wird über den Topic-Namen an genau einen Consumer geroutet, der die eigentliche Geschäftslogik ausführt und den Status anschließend über OperationManagementInterface::changeOperationStatus() aktualisiert. Diese Trennung zwischen Bulk-Metadaten (eine bulk_uuid pro Anfrage) und Operation-Details (viele Operationen pro Bulk) ist der Kern dessen, was die Bulk-API von einer simplen Schleife über die asynchrone Web-API unterscheidet.
4. queue.xml und communication.xml konfigurieren
Damit eine eigene Nachricht überhaupt durch die Message Queue laufen kann, müssen drei Deklarationen zusammenspielen: communication.xml definiert das Topic samt Request-Schema, queue.xml bindet dieses Topic an einen konkreten Exchange und eine Queue, und consumers.xml registriert den PHP-Consumer, der Nachrichten aus dieser Queue konsumiert. Alle drei Dateien liegen im etc-Verzeichnis des jeweiligen Moduls und werden von Magento beim Cache-Flush neu eingelesen.
Ein häufiger Fehler bei eigenen Modulen ist, das Topic zwar in communication.xml zu deklarieren, aber die Bindung in queue.xml zu vergessen. In diesem Fall landet die Nachricht nie in einer Queue und der Consumer läuft leer, ohne eine Fehlermeldung auszugeben, was die Fehlersuche unnötig erschwert. Ebenso wichtig ist der max_messages-Wert in consumers.xml: Er bestimmt, nach wie vielen verarbeiteten Nachrichten sich der Consumer-Prozess sauber beendet, was für den Einsatz mit Supervisor oder systemd essenziell ist, damit Speicherlecks in langlaufenden PHP-Prozessen keine Rolle spielen.
<!-- app/code/Mironsoft/PriceSync/etc/communication.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/communication.xsd">
<topic name="mironsoft.pricesync.bulk.update"
request="Mironsoft\PriceSync\Api\Data\PriceUpdateRequestInterface">
</topic>
</config>
<!-- app/code/Mironsoft/PriceSync/etc/queue.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd">
<broker topic="mironsoft.pricesync.bulk.update" exchange="magento" type="amqp">
<queue name="mironsoft.pricesync.bulk.update" consumer="mironsoftPriceSyncConsumer" consumerInstance="Mironsoft\PriceSync\Model\Queue\PriceUpdateConsumer"/>
</broker>
</config>
<!-- app/code/Mironsoft/PriceSync/etc/consumers.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd">
<consumer name="mironsoftPriceSyncConsumer"
queue="mironsoft.pricesync.bulk.update"
connection="amqp"
max_messages="10000"
handler="Mironsoft\PriceSync\Model\Queue\PriceUpdateConsumer::process"/>
</config>
Nach dem Anlegen oder Ändern dieser drei Dateien ist ein setup:upgrade und ein Neustart der Consumer über bin/magento queue:consumers:start mironsoftPriceSyncConsumer erforderlich, damit die neue Bindung wirksam wird. In produktiven Umgebungen laufen Consumer typischerweise als eigene Supervisor- oder systemd-Services, nicht als Ad-hoc-Kommandozeilenprozesse, damit ein Absturz automatisch zu einem Neustart führt und die asynchrone Verarbeitung nicht unbemerkt zum Erliegen kommt.
5. Eigene asynchrone Endpoints und Consumer bauen
Um eine eigene Service-Contract-Methode asynchron nutzbar zu machen, reicht die reguläre webapi.xml-Deklaration mit einem Repository, das eine save()- oder vergleichbare Methode implementiert. Die eigentliche Verarbeitungslogik gehört jedoch nicht in den Service-Contract selbst, sondern in eine dedizierte Consumer-Klasse, die ConsumerInterface aus \Magento\Framework\MessageQueue implementiert. Diese Klasse wird über consumers.xml registriert und erhält die deserialisierte Nachricht als typisiertes Objekt, wie es in communication.xml deklariert wurde.
Mit PHP 8.4 und Constructor Property Promotion lässt sich eine solche Consumer-Klasse deutlich kompakter schreiben als in älteren Magento-Versionen üblich. Wichtig ist, dass der Consumer Fehler nicht einfach verschluckt, sondern über OperationManagementInterface den Status der zugehörigen Operation aktualisiert, damit der Aufrufer über den Status-Endpoint nachvollziehen kann, ob die Verarbeitung erfolgreich war.
<?php
declare(strict_types=1);
namespace Mironsoft\PriceSync\Model\Queue;
use Magento\AsynchronousOperations\Api\Data\OperationInterface;
use Magento\Framework\EntityManager\EntityManager;
use Magento\Framework\MessageQueue\ConsumerInterface;
use Mironsoft\PriceSync\Api\Data\PriceUpdateRequestInterface;
use Mironsoft\PriceSync\Api\PriceUpdateRepositoryInterface;
use Psr\Log\LoggerInterface;
/**
* Consumer that applies a single price update message from the bulk queue.
*/
final class PriceUpdateConsumer implements ConsumerInterface
{
/**
* @param PriceUpdateRepositoryInterface $priceUpdateRepository Repository applying the actual price change
* @param LoggerInterface $logger Logger used for retriable and non-retriable failures
*/
public function __construct(
private readonly PriceUpdateRepositoryInterface $priceUpdateRepository,
private readonly LoggerInterface $logger,
) {
}
/**
* Processes a single price update message consumed from the bulk queue.
*
* @param PriceUpdateRequestInterface $message Deserialized message matching communication.xml schema
* @return void
*/
public function process(PriceUpdateRequestInterface $message): void
{
try {
// Idempotency guard: skip if this external order/reference id was already applied
$this->priceUpdateRepository->save($message);
} catch (\Magento\Framework\Exception\CouldNotSaveException $exception) {
// Transient error, e.g. lock wait timeout: mark retriable so the consumer retries later
$this->logger->warning('Retriable price update failure', ['sku' => $message->getSku()]);
throw $exception;
} catch (\Throwable $exception) {
// Permanent error, e.g. invalid SKU: mark non-retriable, do not retry indefinitely
$this->logger->error('Non-retriable price update failure', ['sku' => $message->getSku()]);
throw $exception;
}
}
}
In der Praxis kapselt man die eigentliche Fachlogik nicht direkt im Consumer, sondern delegiert an ein Repository, das auch von synchronen Endpoints wiederverwendet werden kann. So bleibt die Business-Logik unabhängig davon, ob sie über einen synchronen REST-Call, eine asynchrone Bulk-API oder einen internen Cronjob ausgelöst wird, ein Prinzip, das Magentos Service-Contract-Architektur ohnehin nahelegt.
6. Operation-Status abfragen und überwachen
Nach dem Absenden eines asynchronen Requests oder eines Bulk-Requests erhält der Client eine bulk_uuid in der HTTP-Antwort. Über den Endpoint GET /rest/V1/bulk/:bulk_uuid/status lässt sich der aktuelle Verarbeitungsstand jeder einzelnen Operation innerhalb dieses Bulk-Vorgangs abfragen. Jede Operation trägt einen Status aus der Konstantenmenge von OperationInterface: STATUS_TYPE_OPEN für noch nicht abgeholte Nachrichten, STATUS_TYPE_COMPLETE für erfolgreich verarbeitete Operationen, STATUS_TYPE_RETRIABLY_FAILED für Operationen, die an einem temporären Fehler gescheitert sind und erneut versucht werden, sowie STATUS_TYPE_NOT_RETRIABLY_FAILED für endgültig fehlgeschlagene Operationen.
Externe Systeme, die eine asynchrone Bulk-API ansprechen, benötigen eine klare Polling-Strategie, um den Status effizient zu überwachen, ohne den Server unnötig zu belasten. Bewährt hat sich ein exponentielles Backoff, beginnend bei etwa 2 Sekunden zwischen den Abfragen und einer oberen Grenze von 30 bis 60 Sekunden, kombiniert mit einem Gesamt-Timeout, nach dessen Ablauf der Vorgang als hängengeblieben markiert und manuell geprüft wird. Wichtig ist, dass ein PIM- oder ERP-System niemals blind auf HTTP-Status 202 als Erfolg interpretiert, sondern den tatsächlichen Operation-Status über den Status-Endpoint abfragt, bevor es die Verarbeitung als abgeschlossen betrachtet.
{
"bulk_uuid": "3f9a1c2e-8b7d-4e21-9a4c-6d2f1e0c8a55",
"operations_list": [
{
"id": 1021,
"bulk_uuid": "3f9a1c2e-8b7d-4e21-9a4c-6d2f1e0c8a55",
"topic_name": "mironsoft.pricesync.bulk.update",
"status": 3,
"result_message": "",
"serialized_data": "{\"sku\":\"WSH-2000\",\"price\":\"49.9000\"}",
"error_code": null
},
{
"id": 1022,
"bulk_uuid": "3f9a1c2e-8b7d-4e21-9a4c-6d2f1e0c8a55",
"topic_name": "mironsoft.pricesync.bulk.update",
"status": 4,
"result_message": "SKU WSH-2001 does not exist",
"serialized_data": "{\"sku\":\"WSH-2001\",\"price\":\"39.9000\"}",
"error_code": 404
}
]
}
Der numerische Wert von status entspricht dabei den Klassenkonstanten: 1 steht für STATUS_TYPE_OPEN, 2 für STATUS_TYPE_COMPLETE, 3 wird in manchen Magento-Versionen für STATUS_TYPE_RETRIABLY_FAILED genutzt und 4 für STATUS_TYPE_NOT_RETRIABLY_FAILED, wobei die exakte Zuordnung je nach Magento-Minor-Version leicht variieren kann und im Zweifel immer gegen die Klassenkonstanten und nicht gegen hartcodierte Zahlen geprüft werden sollte.
7. RabbitMQ vs. MySQL als Message Broker
Magento unterstützt zwei Broker-Implementierungen für die Message Queue: eine MySQL-basierte Queue, die ohne zusätzliche Infrastruktur direkt in den bestehenden Datenbank-Tabellen läuft, und RabbitMQ als dedizierten AMQP-Broker. Für kleine Installationen mit gelegentlichen asynchronen Imports und niedrigem Nachrichtenaufkommen reicht die MySQL-Queue oft aus, weil kein zusätzlicher Dienst betrieben und überwacht werden muss. Die Konfiguration erfolgt einfach dadurch, dass in queue.xml kein type="amqp" gesetzt wird, sondern die Default-Datenbank-Queue greift.
Sobald jedoch produktive Bulk-Verarbeitung mit mehreren parallelen Consumer-Instanzen, hoher Nachrichtenfrequenz oder Anforderungen an garantierte Zustellung ins Spiel kommt, wird RabbitMQ zur praktisch zwingenden Voraussetzung. RabbitMQ bietet echtes Message-Acknowledgement, Priorisierung von Nachrichten, Dead-Letter-Exchanges und eine deutlich bessere Skalierbarkeit bei vielen gleichzeitigen Consumern, weil die MySQL-Queue bei hoher Parallelität schnell zum Locking-Flaschenhals auf der Datenbank wird. Die Konfiguration erfolgt zentral in app/etc/env.php unter dem Schlüssel queue, wo Host, Port, Benutzer, Passwort und virtueller Host des RabbitMQ-Servers hinterlegt werden.
<?php
// app/etc/env.php (excerpt)
return [
'queue' => [
'amqp' => [
'host' => 'rabbitmq.internal.mironsoft.de',
'port' => '5672',
'user' => 'magento',
'password' => 'REPLACE_WITH_SECRET',
'virtualhost' => '/magento-prod',
'ssl' => true,
],
],
];
Horizontale Skalierung von Consumern funktioniert in beiden Setups, ist mit RabbitMQ aber wesentlich zuverlässiger: Mehrere Instanzen desselben Consumers, gestartet über bin/magento queue:consumers:start mironsoftPriceSyncConsumer --pid-file-path=/var/run/consumer1.pid auf verschiedenen Prozessen oder sogar verschiedenen Hosts, teilen sich automatisch die Nachrichten aus derselben Queue, ohne dass eine Nachricht doppelt verarbeitet wird, solange RabbitMQ als Broker fungiert. Bei der MySQL-Queue ist diese Garantie schwächer, weshalb für ernsthafte, produktive asynchrone Bulk-API-Verarbeitung mit mehreren Consumer-Workern RabbitMQ die empfohlene Wahl ist.
8. Fehlerbehandlung, Retries und Idempotenz bei Bulk-Operationen
Jeder Consumer, der über consumers.xml registriert wird, kann über das Attribut max_retries konfiguriert werden, das bestimmt, wie oft eine als retriable markierte Operation erneut zugestellt wird, bevor sie endgültig als STATUS_TYPE_NOT_RETRIABLY_FAILED markiert wird. Wird dieser Wert nicht sinnvoll gesetzt, kann es passieren, dass fehlerhafte Nachrichten endlos zwischen Queue und Consumer zirkulieren und dabei Systemressourcen binden, ohne dass jemals ein Erfolg eintritt. Ein Dead-Letter-Verhalten, bei dem endgültig gescheiterte Nachrichten in eine separate Queue verschoben werden, lässt sich bei RabbitMQ über Dead-Letter-Exchanges realisieren und ist die empfohlene Strategie, um solche Nachrichten für eine spätere manuelle Analyse zu isolieren, statt sie stillschweigend zu verwerfen.
Der wichtigste Aspekt bei Bulk-Operationen ist jedoch Idempotenz. Da eine Nachricht bei einem Retry unter Umständen mehrfach zugestellt wird, etwa weil der Consumer zwischen erfolgreicher Verarbeitung und Acknowledgement abstürzt, muss jede Operation so gestaltet sein, dass eine mehrfache Ausführung keine falschen Ergebnisse erzeugt. In der Praxis bedeutet das, dass jede Operation eine eindeutige Idempotenz-ID tragen sollte, beispielsweise die externe Bestellnummer eines PIM-Systems oder eine UUID, die vom aufrufenden System generiert wird. Der Consumer prüft vor der eigentlichen Verarbeitung, ob diese ID bereits in einer eigenen Tracking-Tabelle vorhanden ist, und überspringt die Operation, falls ja.
Ein Preisupdate, das denselben Datensatz zweimal mit demselben Wert schreibt, ist von Natur aus idempotent und unkritisch. Ein Lagerbestands-Dekrement hingegen, das bei jedem Aufruf den Bestand um eine feste Menge reduziert, ist ohne Idempotenz-Prüfung gefährlich, weil ein Retry den Bestand fälschlicherweise doppelt reduzieren würde. Genau solche nicht-idempotenten Operationen sind der häufigste Grund für schwer nachvollziehbare Dateninkonsistenzen in produktiven Systemen, die asynchrone Verarbeitung mit Retries einsetzen, ohne die Idempotenz der zugrunde liegenden Geschäftslogik vorher sauber zu durchdenken.
9. Praxisbeispiel und Vergleich: Massen-Preisupdate von einem PIM-System
Ein PIM-System, das 10.000 SKUs mit neuen Preisen an Magento überträgt, lässt sich auf drei grundsätzlich verschiedene Arten anbinden. Über synchrone REST-Calls würde das PIM-System 10.000 einzelne PUT-Requests senden, wobei jeder Request einen PHP-FPM-Worker für die Dauer der Datenbankschreibung blockiert und bei realistischen 80 bis 150 Millisekunden pro Request eine Gesamtlaufzeit von 15 bis 25 Minuten allein für die HTTP-Kommunikation entsteht, ganz ohne die zusätzliche Last durch Indexer-Trigger. Über einzelne asynchrone Requests mit X-Asynchronous: true würde das PIM-System ebenfalls 10.000 Requests senden, jeder davon aber sofort mit HTTP 202 beantwortet, sodass sich die Netzwerklast auf den Client-seitigen Verbindungsaufbau reduziert, während die eigentliche Verarbeitung im Hintergrund über Consumer läuft.
Die dritte und für diesen Anwendungsfall optimale Variante ist die Bulk-API: Das PIM-System sendet alle 10.000 Preisänderungen in wenigen großen Batches, etwa zu je 500 Items, an einen Bulk-fähigen Endpoint. Dadurch entstehen statt 10.000 nur 20 HTTP-Requests, jeder mit einer eigenen bulk_uuid, während die serverseitige Zerlegung in einzelne Operationen und deren Verarbeitung über die Consumer parallel und unabhängig vom ursprünglichen HTTP-Request läuft. Der folgende Aufruf zeigt, wie ein solcher Bulk-Request in der Praxis über curl ausgelöst wird.
#!/usr/bin/env bash
set -euo pipefail
TOKEN="$(cat /run/secrets/magento_api_token)"
BASE_URL="https://shop.mironsoft.de/rest/V1"
# Trigger a bulk price update for a batch of 500 SKUs
# Note: X-Asynchronous is implicit for true bulk endpoints; shown here for clarity
curl -s -X POST "${BASE_URL}/products/bySku" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-H "X-Asynchronous: true" \
-d @batch-001-of-20.json | tee response-001.json
# Extract bulk_uuid from the response for later status polling
bulk_uuid="$(jq -r '.bulk_uuid' response-001.json)"
echo "Batch 001 scheduled: ${bulk_uuid}"
# Poll status with simple exponential backoff
wait_seconds=2
for attempt in $(seq 1 10); do
status_response="$(curl -s -H "Authorization: Bearer ${TOKEN}" \
"${BASE_URL}/bulk/${bulk_uuid}/status")"
echo "${status_response}" | jq -c '.operations_list[] | {id, status, error_code}'
sleep "${wait_seconds}"
wait_seconds=$(( wait_seconds < 30 ? wait_seconds * 2 : 30 ))
done
Die folgende Tabelle fasst die drei Ansätze anhand der für dieses Praxisbeispiel relevanten Kriterien zusammen. Sie zeigt, dass die Wahl zwischen synchroner REST-API, asynchroner Web-API und Bulk-API keine reine Geschmacksfrage ist, sondern direkt von Datenvolumen, Latenzanforderungen und der Client-Architektur abhängt.
| Kriterium | Synchrone REST-API | Asynchrone Web-API | Bulk-API |
|---|---|---|---|
| Antwortzeit | Blockiert bis Verarbeitung fertig ist | Sofort (202), Ergebnis über Polling | Sofort (202), ein Poll für viele Items |
| Skalierbarkeit | Begrenzt durch FPM-Worker-Pool | Gut, durch Consumer horizontal skalierbar | Am besten, wenige Requests, viele Operationen |
| Fehlerbehandlung | Direkt im Response, aber Alles-oder-Nichts | Pro Operation über Status-Endpoint | Pro Item innerhalb eines Bulk-Vorgangs |
| Einsatzzweck | Einzelne Live-Interaktionen, Checkout-Calls | Einzelne Requests, die nicht blockieren dürfen | Massendaten, ERP/PIM-Imports, Batch-Jobs |
| Client-Komplexität | Gering, direkte Antwort verarbeiten | Mittel, Polling pro Request nötig | Mittel, Polling pro Batch statt pro Item |
Für das konkrete Beispiel mit 10.000 SKUs bedeutet das: Synchrone REST-Calls sind für diesen Anwendungsfall ungeeignet und riskieren Timeouts sowie blockierte Worker. Einzelne asynchrone Requests lösen das Blockierproblem, erzeugen aber weiterhin 10.000 HTTP-Requests mit entsprechendem Overhead. Die Bulk-API reduziert die Anzahl der Requests um den Faktor der Batch-Größe, während die serverseitige Verarbeitung weiterhin granular pro Operation nachvollziehbar bleibt, und ist damit für dieses Szenario die technisch und operativ überlegene Wahl.
10. Zusammenfassung
Die asynchrone Bulk-API in Magento 2 löst ein klar umrissenes Problem: Synchrone REST-Calls blockieren PHP-FPM-Worker, laufen bei Massendaten in HTTP-Timeouts und beeinträchtigen im schlimmsten Fall den Checkout-Durchsatz. Der Header X-Asynchronous: true verwandelt praktisch jeden bestehenden Endpoint in eine nicht-blockierende Variante, während BulkManagementInterface und OperationInterface ganze Arrays von Items in einzeln nachverfolgbare Operationen mit gemeinsamer bulk_uuid zerlegen. Die Konfiguration über communication.xml, queue.xml und consumers.xml ist dabei reines Deklarations-XML, ohne dass die eigentliche Geschäftslogik verändert werden muss.
Für den produktiven Einsatz sind drei Punkte entscheidend: RabbitMQ statt MySQL-Queue, sobald mehrere Consumer-Instanzen parallel unter Last laufen sollen, ein sauber konfiguriertes Retry- und Dead-Letter-Verhalten, damit fehlerhafte Nachrichten nicht endlos zirkulieren, und vor allem Idempotenz auf Ebene jeder einzelnen Operation, damit Retries keine doppelten Effekte in der Datenbank erzeugen. Wer diese drei Punkte beachtet, kann Massenimporte, Preisupdates und ERP-Anbindungen zuverlässig über die asynchrone Bulk-API abwickeln, ohne die Stabilität des laufenden Shop-Betriebs zu gefährden.
Asynchrone und Bulk-Web-APIs in Magento 2, das Wichtigste auf einen Blick
X-Asynchronous Header
Macht bestehende REST-Endpoints nicht-blockierend, ohne zusätzliche webapi.xml-Deklaration. Antwort ist HTTP 202 mit bulk_uuid.
Bulk-API für Massendaten
BulkManagementInterface zerlegt Arrays in einzelne Operations mit gemeinsamer bulk_uuid. Ein Request statt tausender Einzelaufrufe.
RabbitMQ für Produktion
MySQL-Queue reicht für kleine Installationen. Für mehrere parallele Consumer und hohe Last ist RabbitMQ Pflicht.
Idempotenz bei Retries
Jede Operation braucht eine eindeutige Idempotenz-ID, sonst führen Retries zu doppelten Effekten wie falschen Bestandskorrekturen.
11. FAQ: Asynchrone und Bulk-Web-APIs in Magento 2
1Unterschied asynchrone Web-API vs. Bulk-API?
2Eigene webapi.xml-Route für asynchron nötig?
3Erfolg einer Bulk-Operation erkennen?
4MySQL-Queue oder RabbitMQ?
5Consumer stürzt während Verarbeitung ab?
6max_retries konfigurieren?
7Eigene Topics definieren?
8Warum Idempotenz-IDs verwenden?
9Wie viele Items pro Bulk-Request?
10Consumer horizontal skalieren?
Mironsoft
Magento 2 Web-API, Message Queue und Bulk-Verarbeitung
ERP- oder PIM-Anbindung, die Massendaten zuverlässig verarbeitet?
Wir konzipieren und implementieren asynchrone Bulk-API-Anbindungen für Magento 2, inklusive Message-Queue-Konfiguration, eigenen Consumern und sauberer Retry- und Idempotenz-Strategie für euren produktiven Betrieb.
Architektur-Review
Analyse bestehender Integrationen und Empfehlung sync vs. async vs. Bulk-API
Consumer-Entwicklung
Eigene Topics, Consumer und Idempotenz-Logik nach Magento-Service-Contract-Standards
RabbitMQ-Setup
Skalierbare Message-Queue-Infrastruktur für produktive Bulk-Verarbeitung