Asynchronous Communication in PHP

Move any handler to async with one attribute. Atomic outbox in one DBAL transaction. Per-handler failure isolation. #[Delayed], #[Priority], #[TimeToLive] consistent across RabbitMQ, Kafka, SQS, Redis, DBAL, Messenger transports, and Laravel Queue channels — handler code is broker-agnostic.

Composer package · Laravel or Symfony · PostgreSQL or MySQL · RabbitMQ, Kafka, SQS, Redis, or DBAL outbox

How it compares

PHP alternatives for this category

Each row names what you'd need to add to match Ecotone, and the architectural ceiling that integration cannot fix.

Symfony Messenger alone

Retry is per-transport. No outbox primitive. Single-tenant per app.

  • Handlers use `#[AsMessageHandler]` and dispatch via `MessageBusInterface`; transports configured by DSN (`MESSENGER_TRANSPORT_DSN`).
  • **Retry strategy is per-transport, not per-handler** — two handlers on the same message share the retry envelope, and a failing handler's retry re-runs every handler.
  • Doctrine transport approximates outbox but is not transactional with the business aggregate by default.
  • Priority is achieved by separate transports, not first-class.
  • TTL/delay supported on AMQP/SQS transports, not uniformly across backends.
  • Multi-tenant routing has no first-class API — single-tenant per application.
Laravel Queues / Horizon

Job runner. No command/event/query separation. No outbox. Horizon Redis-only.

  • Job class implements `Illuminate\Contracts\Queue\ShouldQueue`, uses `Queueable` trait, defines `handle(...)`.
  • No native command / event / query separation — one job = one handler; fan-out to multiple subscribers is hand-wired.
  • No outbox primitive.
  • Multi-broker is a per-dispatch connection choice, not a channel abstraction above transport.
  • Priority is the ordered `--queue=high,default` worker arg, not first-class.
  • Delay capped at 15 minutes on SQS. Horizon is Redis-only.
Ecotone

From #[Asynchronous] on day one to multi-broker outbox at scale.

  • On day one, `#[Asynchronous('channel')]` moves any handler to async — the same handler code runs on RabbitMQ, Apache Kafka, Amazon SQS, Redis, DBAL, Symfony Messenger transports, or Laravel Queue channels.
  • As volume grows, `CombinedMessageChannel` splits outbox storage from execution: the database holds the outbox (one poller drains it), the broker carries execution (consumers scale horizontally).
  • Per-handler failure isolation by default — each subscriber gets its own copy of the message, so one failing handler doesn't trigger sibling re-runs on retry.
  • `#[Delayed]`, `#[Priority]`, `#[TimeToLive]`, and scheduled messages have one consistent attribute model across brokers.
  • Dynamic channels route per-tenant at runtime via headers without redeploying.
  • The dispatch you started with composes upward; nothing is replaced as you scale.

Frequently asked questions

Haven’t found what you’re looking for? Contact us

Symfony Messenger and Laravel Queues are mature dispatchers — they ship transports, retry/failure transport, and middleware. They don't ship the architectural primitives that production async messaging needs above the dispatch layer: transactional outbox, per-handler failure isolation (each subscriber gets its own copy of the message), deduplication, multi-tenant routing, or aggregate / saga / event-store / projection support. Ecotone adds those primitives and uses Messenger transports and Laravel Queue channels as the underlying transport when they're already configured — existing investments stay; new architectural capability is added.
"CombinedMessageChannel::create('outbox_sqs', ['database_channel', 'amazon_sqs_channel'])" writes the message into the database in the same DBAL transaction as the business state change, then dispatches actual handler execution onto SQS (or any broker). One outbox poller drains the database; many consumers handle broker-side execution. If the business transaction rolls back, the atomicy of the changes is ensured. The dual-write problem disappears at the primitive level.
When multiple handlers subscribe to the same event, Ecotone delivers a copy of the message to each handler. If one handler fails, only that handler retries — siblings are not affected. In Symfony Messenger's single-envelope model, the first throwing consumer aborts dispatch for siblings, and retry re-runs every consumer on the same envelope. Per-handler isolation eliminates that class of duplicate side effects.
Yes. Handlers use #[Asynchronous('channel-name')] — broker-agnostic. The channel's underlying transport is bound in one #[ServiceContext] configuration method. Swap RedisBackedMessageChannelBuilder for AmqpBackedMessageChannelBuilder or SqsBackedMessageChannelBuilder — handler code unchanged.

Be part of the change with EcotoneCurve

Unleash the power of Messaging in PHP
and push productivity to the higher level

Get started
Gradient
Shapes 1
Shapes 2
DiscordTwitterSupport and ContactTelegram