Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Messenger Retry Bundle Laravel Package

pccomponentes/messenger-retry-bundle

A Symfony bundle for Messenger that adds flexible retry handling for failed or transiently failing messages. Configure retry strategies and requeue messages through Symfony's transports and failure transport, keeping async workloads resilient without custom glue code.

View on GitHub
Deep Wiki
Context7

Getting Started

The 5-minute setup (Symfony app — the package's native home):

  1. composer require pccomponentes/messenger-retry-bundle
  2. Register the bundle in config/bundles.php:
    return [
        // ...
        Pccomponentes\MessengerRetryBundle\MessengerRetryBundle::class => ['all' => true],
    ];
    
  3. Confirm you already have symfony/messenger configured with at least one async transport and a failure transport — this bundle augments that setup, it does not replace it.
  4. Define a retry policy against a transport, e.g. an SMTP or third-party HTTP transport:
    framework:
        messenger:
            transports:
                async_http:
                    dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                    retry_strategy:
                        max_retries: 5
                        delay: 1000
                        multiplier: 2
                        max_delay: 60000
            failure_transport: failed
    

Important context before step 1 if you are coming from Laravel (see Gotchas): this is a Symfony bundle, not a Laravel package. It is designed to be registered via Symfony's Kernel / bundle system. To use it inside a Laravel app you first need a Symfony Messenger bridge (e.g. laravel/symfony-messenger-style integrations) that boots a Symfony container for Messenger — otherwise the bundle's Extension and DI compiler passes will never run. If you are simply using Laravel queues, stop here and use $tries / $backoff / failed_jobs instead.

First use case: wrap an unreliable outbound integration — a flaky payment provider, a slow email API, a partner webhook — with a bounded exponential retry so transient 5xx/connection errors do not become lost work, and exhausted messages land in a failed transport you can inspect and replay.

Where to look first in the repo: the bundle's Extension / configuration tree (to see the exact keys available under retry_strategy) and the Messenger middleware it registers in the pipeline. There is no elaborate docs site — the config schema is the documentation, so read DependencyInjection/Configuration.php first.

Implementation Patterns

Pattern 1 — Per-transport policy (the primary use case). Put the retry strategy on the transport DSN, not on the dispatcher. Every message routed to async_http inherits the policy automatically:

async_http:
    dsn: '%env(HTTP_QUEUE_DSN)%'
    retry_strategy:
        max_retries: 5
        delay: 1000
        multiplier: 2
        max_delay: 60000

Pattern 2 — Tiered retry classes. Do not use one policy for everything. Create two or three transports with different aggressiveness:

  • async_fast — cheap, idempotent work: max_retries: 2, delay: 500, multiplier: 2.
  • async_external — third-party APIs: max_retries: 5, delay: 2000, multiplier: 3, max_delay: 300000.
  • async_expensive — batch/ETL: max_retries: 8, delay: 5000, multiplier: 2, max_delay: 3600000.

Route messages by dispatch-time $bus (async_external) rather than relying on per-message config. This keeps the policy in YAML (visible to ops) and out of your DTOs.

Pattern 3 — Per-message overrides. Where the bundle exposes per-message retry (via a marker interface / attribute on the message class), use it sparingly — only when a single message has a materially different failure profile from its transport peers. Document the override in the message class docblock so a future reader knows the transport default does not apply.

Pattern 4 — Failure transport as a first-class workflow, not an afterthought.

failure_transport: failed

Then build a messenger:failed:show / messenger:failed:retry habit. Your on-call runbook should include: “check failed transport count; if spiking, look at the shared dependency, not the messages.” A retry bundle is only as good as your ability to see the terminal failures.

Pattern 5 — Handler idempotency as a paired discipline. Retry without idempotency is a data-corruption generator. For each retried handler:

  • Upsert (not insert) by a business key.
  • Or persist a processed-message ledger keyed by message id before doing the side effect.
  • Or make the external call idempotent (idempotency keys on Stripe-style APIs).

If the handler is not idempotent, do not give it max_retries > 0.

Pattern 6 — Middleware composition. The bundle plugs into the Messenger middleware stack, so it composes with your own middleware (tracing, logging, tenancy, rate-limiting). Register yours with priority below the retry middleware if you want the retry to re-enter your middleware on each attempt (typical for tracing), or above it if you want the middleware to see only the logical dispatch (typical for metrics — count one dispatch, not N retries).

Workflow for adopting it on an existing app:

  1. Inventory current transports and their retry behaviour (many apps have none).
  2. Classify each transport by idempotency and cost.
  3. Apply a policy per class.
  4. Add a failed transport before turning retries on — no exceptions.
  5. Ship to staging with a deliberately failing integration and verify the backoff curve in logs.
  6. Roll out transport by transport, not all at once.

Gotchas and Tips

The framework mismatch — read this first. The package is a Symfony bundle. A Laravel app that has not adopted Symfony Messenger will not be able to use it at all: there is no service provider here, no config/messenger-retry.php, no php artisan command. If your Laravel codebase relies on ShouldQueue / Illuminate\Queue, the correct answer is Laravel's built-in $tries, $backoff (array for explicit backoff), and failed_jobs table. Adopt this bundle only if you have deliberately introduced Symfony Messenger for cross-framework messaging or are on a Symfony app.

If you are on Laravel and using Symfony Messenger via a bridge:

  • The bridge must dispatch the full Messenger middleware pipeline. Some lightweight Laravel bridges short-circuit middleware for sync dispatch — your retry policy will silently not apply. Verify with a failing handler and check that the retry count actually increments.
  • Avoid php artisan config:cache surprises: the bundle reads Symfony-style YAML/DI config, not Laravel's config/*.php. Your config:cache will not invalidate it, but a stale Symfony container cache will. Clear both.
  • Laravel's Horizon and failed_jobs do not know about your Messenger failed transport. You now have two failure surfaces to monitor. Instrument both.

Backoff semantics and jitter. Exponential backoff (delay × multiplier^attempt, capped at max_delay) tends to produce synchronization — a thousand messages that all failed at once will all retry at once. The bundle does not document jitter. If your failures are correlated (a downstream outage), add a randomized delay around max_delay via a custom middleware, or accept the “thundering herd on recovery” risk consciously.

max_delay is your safety valve. Without it, multiplier: 3 at attempt 10 gives you a multi-hour wait. Always set max_delay. A good rule of thumb: max_delay should be shorter than your business SLA for the message (e.g. “password reset email must arrive within 30 min” → max_delay: 60000, max_retries: 4).

Message serialization is coupled to retries. Retried messages are re-read from the transport. If your message holds a Doctrine entity or an unserializable resource, the second attempt will fail deserialization — not the first. Test by dispatching a message, letting it fail once, and inspecting the failed transport payload. Store scalar IDs, not entities.

Failure transport routing is per-bus. If you have multiple buses, confirm each one has a failure transport, or max_retries exhaustion will drop messages on the floor for the ones that don't.

Semver warning from the upstream 2.1.1 release. Symfony 7 support was shipped as a patch bump. If you float ^2.1, a composer update can pull a version with a new Symfony major in the constraint. Pin the package (2.1.1) if you are not actively migrating to Symfony 7, and review composer.lock diffs in PRs.

Debugging the retry loop.

  • php bin/console messenger:stats (or the Laravel equivalent via the bridge) shows queue depth per transport.
  • messenger:failed:show <transport> lists terminal failures.
  • Add a LoggerInterface injection in a middleware and log attempt, delay, and originalMessageId — the bundle does not emit much by default.
  • Use messenger:consume -vv in staging; you will see Retrying lines from Symfony's own retry middleware, which this bundle configures.

Extension points to use rather than fork.

  • A custom MiddlewareInterface for jitter, tracing, or dead-letter tagging — registered above/below the retry middleware.
  • A custom RetryStrategyInterface on the transport if you need non-exponential behaviour (e.g. fixed delay for rate-limited APIs). This is Symfony's own extension point and the bundle respects it.
  • Transport-level RetryStrategyInterface is per-transport, so you can mix the bundle's default with your own on a rate_limited transport without conflict.

Operational tip that pays for itself: before enabling retries on a transport, publish the expected backoff curve (attempt → delay) in the team runbook. When an incident fires, the on-call engineer needs to know whether the message is between attempts (leave it alone) or exhausted (replay it). The bundle does not surface this; you must.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource