messenger-async
Skill35 starsChanged 5 months agoMITView on GitHub
Use when implementing async processing, CQRS, or event-driven flows in Symfony with the Messenger component — message/handler pairs, transports, routing, retries, failure queues, and the outbox pattern. Use when the task mentions Messenger, message bus, async, queue, command/query handler, or background job.
In this repository16 other files it gives its agents.
- api-platform-resourcesSkill
- bundle-organizationSkill
- console-commandsSkill
- doctrine-migrationsSkill
- doctrine-ormSkill
- doctrine-query-optimizationSkill
- domain-driven-designSkill
- dto-and-validationSkill
- hexagonal-architectureSkill
- jwt-authenticationSkill
- layered-architectureSkill
- problem-details-rfc9457Skill
- rest-controller-conventionsSkill
- security-authenticationSkill
- testing-pyramidSkill
- transactional-patternsSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 89
Run a worker to consume async messages:
- Very specific: line 137
Keep handlers thin — delegate to an application service (see layered-architecture).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Symfony Messenger (Async & CQRS)
Message + handler pairs
A message is a plain, immutable DTO. A handler is a class with #[AsMessageHandler] and an __invoke() typed to the message. One handler per command.
// Command message — immutable intent
final readonly class PlaceOrder
{
/** @param array<int, array{productId: string, quantity: int}> $items */
public function __construct(
public string $customerEmail,
public array $items,
) {}
}// Handler
#[AsMessageHandler]
final class PlaceOrderHandler
{
public function __construct(private readonly OrderService $orders) {}
public function __invoke(PlaceOrder $command): void
{
$this->orders->placeOrder($command);
}
}// Dispatch from a controller/service
$this->bus->dispatch(new PlaceOrder($request->customerEmail, $request->items));Separate buses for command / query / event (CQRS)
# config/packages/messenger.yaml
framework:
messenger:
default_bus: command.bus
buses:
command.bus: # one handler, returns void/id
middleware: [validation, doctrine_transaction]
query.bus: # one handler, returns a read model
event.bus: # zero-or-many handlers, fan-out
default_middleware: allow_no_handlers- Commands: change state, exactly one handler, named imperatively (
PlaceOrder). - Queries: return data, one handler, no side effects (
GetOrderSummary). - Events: something happened, many subscribers allowed, past tense (
OrderPlaced).
Transports & routing
Sync by default; route the slow/external work to an async transport.
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%' # doctrine:// , amqp:// , redis://
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2 # exponential backoff
failed: 'doctrine://default?queue_name=failed'
failure_transport: failed
routing:
App\Message\Command\PlaceOrder: async
App\Message\Event\OrderPlaced: asyncRun a worker to consume async messages:
php bin/console messenger:consume async --time-limit=3600 --memory-limit=256MIn production, supervise the worker (systemd / Supervisor) and recycle it (--time-limit) to avoid leaks.
Retries & the failure transport
- Transient failures (network blip) → automatic retry with backoff.
- Permanent failures land in the
failedtransport instead of being lost.
php bin/console messenger:failed:show # inspect
php bin/console messenger:failed:retry # replay after a fix
php bin/console messenger:failed:remove <id> # discardThrow UnrecoverableMessageHandlingException to skip retries for errors that will never succeed (bad payload, business rule violation).
Idempotency
Async messages can be delivered more than once (retry, at-least-once transports). Handlers must be idempotent — process by a natural key and no-op if already done.
public function __invoke(ChargePayment $message): void
{
if ($this->payments->existsByIdempotencyKey($message->idempotencyKey)) {
return; // already processed — safe to receive twice
}
$this->gateway->charge($message);
}Outbox pattern: atomic DB write + reliable dispatch
Dispatching a message after flush() can lose it if the process dies in between; dispatching before can emit an event for a transaction that then rolls back. The Doctrine transport with the DispatchAfterCurrentBusMiddleware (built in) handles the in-bus case; for cross-process reliability, persist the message in the same transaction (outbox) and let a relay/worker publish it.
// Dispatch within the handler's transaction; the transport row commits atomically.
$this->bus->dispatch(new OrderPlaced($order->getId()), [new DispatchAfterCurrentBusStamp()]);Conventions
- Messages are immutable, serializable DTOs — no entities, no closures, no services inside them.
- Pass IDs, not Doctrine entities — the entity may be stale by the time the worker runs; reload it in the handler.
- Keep handlers thin — delegate to an application service (see
layered-architecture). - One handler per command; events may have many.
- Name by intent: commands imperative, events past tense.
Gotchas
- Agent puts a Doctrine entity inside a message — pass the ID and reload in the handler; entities don't serialize cleanly and go stale.
- Agent writes non-idempotent handlers — async is at-least-once; guard with an idempotency key.
- Agent never configures a
failure_transport— failed messages vanish; add thefailedtransport. - Agent retries permanent failures forever — throw
UnrecoverableMessageHandlingExceptionfor non-retryable errors. - Agent dispatches an event before
flush()(or after, unguarded) — useDispatchAfterCurrentBusStamp/ outbox for atomicity. - Agent runs handlers synchronously for slow/external work — route those messages to an async transport.
- Agent forgets the worker needs supervising + memory/time limits in prod.
- Agent puts business logic in the handler — delegate to a service; the handler just adapts the message.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

