> For the complete documentation index, see [llms.txt](https://docs.ecotone.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ecotone.tech/modelling/asynchronous-handling/non-blocking-batched-delivery.md).

# Non-blocking Batched Delivery

When your application publishes messages to a Message Broker, each send is a full network round trip: serialize, write, and block until the Broker confirms. Publish a thousand messages and you pay for a thousand round trips, one after another.

Non-blocking Batched Delivery changes that equation. Messages are fired to the Broker without waiting for individual confirmations, multiple messages are combined into single Broker writes, and confirmations are collected once for the whole set - right before your transaction commits. The result is a multiplier on publishing throughput, without giving up a single delivery guarantee.

**You'll know you need this when:**

* A single Command results in many Events, and publishing them one by one dominates the request time
* You run imports, migrations or ETL jobs that push thousands of messages to a Broker
* Your Outbox or high-volume workflow is bottlenecked on publishing latency, not on processing
* You want fire-and-forget publishing speed, yet nothing may be silently lost

{% hint style="success" %}
Non-blocking Batched Delivery is available as part of **Ecotone Enterprise.**
{% endhint %}

## How it works

With synchronous publishing, every message follows the pattern: send, wait for Broker confirmation, send the next one. The waiting dominates - the Broker is mostly idle while your application blocks on network latency.

High Throughput Publishing attacks this with two independent mechanisms:

1. **Batch publishing** - messages published within the same execution scope are combined and written to the Broker together (a single multi-row insert for DBAL, a batched publish for RabbitMQ, one batch request per 10 messages for SQS, a single script execution for Redis, producer lingering for Kafka)
2. **Non-blocking confirmation** - instead of blocking per message, the message is handed to the Broker and its delivery confirmation is collected later

Confirmations are then **awaited before commit** - all outstanding deliveries are resolved before your transaction commits, so a successful Command execution means every published message is confirmed by the Broker.

### What each provider offers

Batching is available everywhere. Non-blocking confirmation requires a transport that can hand off a write and learn its outcome later, which not every store can do:

| Provider        | Batch publishing                                                                                        | Non-blocking confirmation                             |
| --------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| RabbitMQ (AMQP) | one publisher-confirms round trip per batch; a single socket write on the **AmqpLib driver**, see below | Yes - confirms awaited at scope end                   |
| Kafka           | producer lingering                                                                                      | Yes - delivery reports awaited at scope end           |
| Amazon SQS      | batch send requests                                                                                     | Yes - responses awaited at scope end                  |
| Database (DBAL) | multi-row insert                                                                                        | No - the insert blocks until the database confirms it |
| Redis           | single scripted round trip                                                                              | No - the round trip blocks until Redis confirms it    |

DBAL and Redis therefore accept no parameters: batching is the whole of what they can offer, and their configuration says so.

{% hint style="warning" %}
**RabbitMQ writes an AMQP batch to the socket in a single go only on the AmqpLib connection factory** (`Enqueue\AmqpLib\AmqpConnectionFactory`). With the AmqpExt factory each message is written individually, yet batch publishing still contributes substantially: the whole set travels through the messaging pipeline as one message, and the publisher confirms are coalesced into a single wait. Non-blocking confirmation works on both drivers. AmqpExt remains the faster driver in absolute terms on every scenario, as its C extension encodes the protocol far cheaper than a pure PHP implementation.
{% endhint %}

## Enabling High Throughput Publishing

Not calling the method leaves publishing unchanged. Calling it enables every mechanism the provider supports:

```php
#[ServiceContext]
public function orderChannel()
{
    return AmqpBackedMessageChannelBuilder::create("orders")
                ->withHighThroughputPublishing();
}
```

and for Message Publisher:

```php
#[ServiceContext]
public function messagePublisher()
{
    return AmqpMessagePublisherConfiguration::create()
                ->withDefaultRoutingKey("orders")
                ->withHighThroughputPublishing();
}
```

Each mechanism can be turned off by name, and the confirmation timeout tuned:

```php
AmqpBackedMessageChannelBuilder::create("orders")
    ->withHighThroughputPublishing(
        batchPublishing: true,
        nonBlockingConfirmation: false, // batch the writes, but confirm inline
        confirmationTimeoutInMilliseconds: 5000,
    );
```

The same method is available on `KafkaMessageChannelBuilder`, `SqsBackedMessageChannelBuilder`, `RedisBackedMessageChannelBuilder` and `DbalBackedMessageChannelBuilder`, and on every Message Publisher configuration. On DBAL and Redis it takes no arguments.

## Publishing from Business Code

The most common scenario requires no API changes at all. When your Command Handler publishes Events to an asynchronously published channel, Ecotone collects them and delivers them as one batch:

```php
#[CommandHandler]
public function placeOrder(PlaceOrder $command, EventBus $eventBus): void
{
    // each Event goes to the "orders" channel
    $eventBus->publish(new OrderWasPlaced($command->orderId));
    $eventBus->publish(new PaymentWasRequested($command->orderId));
    $eventBus->publish(new NotificationWasScheduled($command->orderId));
}
```

All three Events are written to the Broker in a single batched operation, and their confirmations are awaited together before the Command Bus returns. Your business code stays exactly the same - enabling `withHighThroughputPublishing()` on the channel is the only change.

## Publishing with Futures

For explicit control, `MessagePublisher` exposes `publishDeferred`, which fires the message and returns a `Future`:

```php
$future = $messagePublisher->publishDeferred($orderData); // 1

// do other work while the Broker processes the delivery

$future->resolve(); // 2
```

1. The message is sent to the Broker immediately, but the confirmation is not awaited
2. `resolve()` awaits the delivery confirmation - it throws `PublishingFailedException` if the Broker rejected the message

This enables pipelining: fire many publishes, let the Broker work on all of them concurrently, then resolve the Futures at the end:

```php
$futures = [];
foreach ($chunkedOrders as $batch) {
    $futures[] = $messagePublisher->publishDeferred($batch);
}

foreach ($futures as $future) {
    $future->resolve();
}
```

{% hint style="info" %}
You never risk losing a message by forgetting to resolve a Future. Ecotone awaits all unresolved deliveries before the surrounding transaction commits, and flushes any remaining ones on application shutdown.
{% endhint %}

{% hint style="warning" %}
`publishDeferred` requires non-blocking confirmation, so it is not available on DBAL and Redis Publishers - there is nothing to defer, as the write blocks until the store confirms it. Publish batches there with `convertAndSend`, shown below.
{% endhint %}

## Batch Messages

To publish a set of messages as one explicit unit, use `BatchMessage`:

```php
$messagePublisher->convertAndSend(
    BatchMessage::constructEmpty()
        ->append($firstOrder)
        ->append($secondOrder, ['priority' => '5']) // 1
        ->append($reminder, [MessageHeaders::DELIVERY_DELAY => 60000]) // 2
        ->append($liveUpdate, [MessageHeaders::TIME_TO_LIVE => 5000]) // 3
);
```

1. Each entry carries its own headers
2. Entries can be individually delayed
3. Entries can individually expire

The whole batch is delivered to the Broker in a single operation, yet each entry keeps its own metadata, delay and time to live. Sending a `BatchMessage` requires batch publishing to be enabled - otherwise Ecotone fails at configuration time telling you so.

On providers that support non-blocking confirmation, a batch can be published as a Future instead, so the whole set is confirmed later:

```php
$messagePublisher->publishDeferred(
    BatchMessage::constructEmpty()
        ->append($firstOrder)
        ->append($secondOrder)
)->resolve();
```

## Delivery Guarantees

Speed without safety would be no gain at all. High Throughput Publishing keeps the full set of Ecotone's delivery guarantees:

* **Confirmed before commit** - all pending deliveries are awaited before the transaction commits. A Command that finished successfully means every published message is safely stored in the Broker
* **Per-message failure attribution** - when part of a batch fails, Ecotone knows exactly which messages failed. Retries redeliver only the failed ones - already delivered messages are never duplicated
* **Error Channel routing** - messages that exhaust retries are routed to your [Error Channel or Dead Letter](/modelling/recovering-tracing-and-monitoring/resiliency/error-channel-and-dead-letter.md) individually, each as a separate, replayable message
* **No silent loss** - deliveries that were never explicitly resolved are awaited at the transaction boundary and flushed on shutdown, with failures logged and routed

For the details of send-path resiliency, see [Resilient Sending](/modelling/recovering-tracing-and-monitoring/resiliency/resilient-sending.md).

## The Throughput Multiplier

Publishing 10,000 messages to a Broker from a single PHP process, single synchronous sends vs High Throughput Publishing, measured on a local Docker setup (CLI opcache and JIT enabled; RabbitMQ on the AmqpExt driver):

| Provider        | 10,000 synchronous sends | High Throughput Publishing | Multiplier |
| --------------- | ------------------------ | -------------------------- | ---------- |
| Kafka           | 1,105 ms                 | 64 ms - 157,266 msg/sec    | **17.4x**  |
| Amazon SQS      | 15,152 ms                | 1,325 ms - 7,549 msg/sec   | **11.4x**  |
| RabbitMQ (AMQP) | 727 ms                   | 149 ms - 67,143 msg/sec    | **4.9x**   |
| Redis           | 311 ms                   | 86 ms - 115,638 msg/sec    | **3.6x**   |
| Database (DBAL) | 609 ms                   | 186 ms - 53,711 msg/sec    | **3.3x**   |

These numbers come from a local network setup, where round trips are cheapest. In production, where the Broker sits behind real network latency, every eliminated round trip is worth more - the multiplier grows with the distance to your Broker and with the volume published per scope.

## Materials

### Links

* [Message Publisher](/modelling/microservices-php/message-publisher.md) \[Documentation]
* [Delivery Semantics and Guarantees](/modelling/recovering-tracing-and-monitoring/delivery-semantics-and-guarantees.md) \[Documentation]


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ecotone.tech/modelling/asynchronous-handling/non-blocking-batched-delivery.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
