Async and Bulk APIs
Add async or async/bulk to a REST route and Magento queues the work, hands back a bulk_uuid, and reports progress separately.
POST /rest/<store-view-code>/async/bulk/V1/productsEvery write route in the REST API has an asynchronous twin. Put async before V1 and the request is turned into a queue message instead of being executed: POST /rest/default/async/V1/products. Put async/bulk there and the payload becomes an array, one message per item: POST /rest/default/async/bulk/V1/products. Every route except GET gets a twin, because a GET has nothing to deliver later.
What comes back
The response says accepted, not done: the status code is 202 Accepted, not 200. It carries a bulk_uuid and a request_items array, each item with an id, a data_hash and status: accepted. Messages are published to topics named after the service method, such as async.magento.catalog.api.productrepositoryinterface.save.POST, and the consumer async.operations.all executes them. With no consumer running, the call still returns 202 and nothing happens.
Checking progress
Status lives on its own GET routes: /V1/bulk/:bulkUuid/status, /V1/bulk/:bulkUuid/detailed-status, and /V1/bulk/:bulkUuid/operation-status/:status. Per-operation status values are 1 complete, 2 failed and retriable, 3 failed and not retriable, 4 open, 5 rejected. The summary for a whole bulk is 0 not started, 1 in progress, 2 finished successfully, 3 finished with failure.
When to reach for it
Use async for writes whose result the caller does not need in the response: price feeds, stock feeds, catalogue syncs. Keep synchronous calls for anything whose body you read. The admin grid for bulk actions, under System > Action Logs, is Adobe Commerce only; the status endpoints above are in Open Source.
Example
<?php
// app/code/Vendor/Feed/Console/Command/BulkStatusCommand.php
declare(strict_types=1);
namespace Vendor\Feed\Console\Command;
use Magento\AsynchronousOperations\Api\BulkStatusInterface;
use Magento\Framework\Bulk\BulkSummaryInterface;
use Magento\Framework\Bulk\OperationInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class BulkStatusCommand extends Command
{
public function __construct(
private readonly BulkStatusInterface $bulkStatus,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
$this->setName('vendor:feed:bulk-status')
->setDescription('Report the status of one bulk operation')
->addArgument('uuid', InputArgument::REQUIRED, 'The bulk_uuid');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$uuid = (string)$input->getArgument('uuid');
$summary = $this->bulkStatus->getBulkStatus($uuid);
$retriable = $this->bulkStatus->getOperationsCountByBulkIdAndStatus(
$uuid,
OperationInterface::STATUS_TYPE_RETRIABLY_FAILED
);
$output->writeln('summary: ' . $summary);
$output->writeln('retriable failures: ' . $retriable);
return $summary === BulkSummaryInterface::FINISHED_SUCCESSFULLY ? 0 : 1;
}
}
// bin/magento vendor:feed:bulk-status fbfca270-7a90-4c4e-9f32-d6cf3728cdc7
// summary: 2
// retriable failures: 0When to use it
- Accept a supplier's hourly price feed over REST without timing out the caller.
- Queue 2,000 stock updates and poll one bulk_uuid for completion.
- Keep a reporting service method synchronous while the rest of the module goes async.
- Report to an integration partner which of their submitted items failed and can be retried.
More examples
Sending a bulk request and reading the status
The body of a bulk call is an array of the bodies you would have posted one at a time, and the endpoint splits it into one queue message per element. Note that errors is false even when an item will later fail: validation happens in the consumer, not in the request. The status routes are declared against an ACL resource that only Adobe Commerce's Magento_Logging module provides, so on Open Source call them with an integration granted all resources.
curl -X POST "https://example.com/rest/default/async/bulk/V1/products" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '[{"product":{"sku":"SKU-1","price":29}},{"product":{"sku":"SKU-2","price":31}}]'
# {
# "bulk_uuid": "fbfca270-7a90-4c4e-9f32-d6cf3728cdc7",
# "request_items": [
# {"id": 0, "data_hash": "9c1bd4bf...", "status": "accepted"},
# {"id": 1, "data_hash": "a1f03e77...", "status": "accepted"}
# ],
# "errors": false
# }
bin/magento queue:consumers:start async.operations.all --max-messages=100 --single-thread
curl "https://example.com/rest/default/V1/bulk/fbfca270-7a90-4c4e-9f32-d6cf3728cdc7/status" \
-H "Authorization: Bearer $TOKEN"Keeping a service out of the async API
Async twins are generated for you from webapi.xml, which is not always wanted: a method that returns a report, or one that must run inside the caller's transaction, should stay synchronous. synchronousInvocationOnly removes its async route. The data element caps how many items one bulk request may carry, which protects the queue from a client that posts fifty thousand rows in one call. Run bin/magento cache:flush after editing this file.
<!-- app/code/Vendor/Feed/etc/webapi_async.xml -->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_WebapiAsync:etc/webapi_async.xsd">
<service class="Vendor\Feed\Api\ReportInterface" method="run">
<synchronousInvocationOnly>true</synchronousInvocationOnly>
</service>
<route url="async/bulk/V1/products" method="POST" alias="asyncBulkProducts">
<data input-array-size-limit="500"/>
</route>
</services>When an import belongs here
The async bulk API suits a steady stream of writes from another system: it applies the same validation as the synchronous endpoints, one entity at a time, so a bad row fails alone instead of aborting a file. It is the wrong tool for a one-off 100,000-row catalogue load, where the CSV importer's bunched writes are far faster. Either way, set price and inventory indexers to Update by Schedule or each message triggers reindex work of its own.
# 2,000 price changes, queued, four workers draining them
curl -X POST "https://example.com/rest/default/async/bulk/V1/products" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data-binary @prices.json
# make sure the queue is being drained
bin/magento queue:consumers:list | grep async.operations.all
# indexes catch up on their own only in Update by Schedule
bin/magento indexer:show-mode catalog_product_price
Discussion