Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Retry Delay & Retry Strategy

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

By default, a message will be retried three times then lost forever. Well... in a few minutes... I'll show you how you can avoid even those messages from being lost.

Anyways... the process... just works! And it's even cooler than it looks at first. It's a bit hard to see - especially because there's a sleep in our handler - but this message was sent for retry #3 at the 13 second timestamp and it was finally handled again down at the 17 second timestamp - a 4 second delay. That delay was not caused by our worker just being busy until then: it was 100% intentional.

Check it out: I'll hit Ctrl+C to stop the worker and then run:

php bin/console config:dump framework messenger

This should give us a big tree of "example" configuration that you can put under the framework messenger config key. I love this command: it's a great way to find options that you maybe didn't know existed.

Cool! Look closely at the transports key - it lists an "example" transport below with all the possible config options. One of them is retry_strategy where we can control the maximum number of retries and the delay that should happen between those retries.

This delay number is smarter than it looks: it works together with the "multiplier" to create an exponentially growing delay. With these settings, the first retry will delay one second, the second 2 seconds and the third 4 seconds.

This is important because, if a message fails due to some temporary issue - like connecting to a third-party server - you might not want to try again immediately. In fact, you might choose to set these to way higher values so that it retries maybe 1 minute or even a day later.

Let's also try a similar command:

php bin/console debug:config framework messenger

Instead of showing example config, this tells us what our current configuration is, including any default values: our async transport has a retry_strategy, which is defaulting to 3 max retries with a 1000 millisecond delay and a multiplier of 2.

Configuring the Delay

Let's make this a bit more interesting. In the handler, let's make it always fail by adding || true.

... lines 1 - 13
class AddPonkaToImageHandler implements MessageHandlerInterface, LoggerAwareInterface
... lines 16 - 30
public function __invoke(AddPonkaToImage $addPonkaToImage)
... lines 33 - 46
if (rand(0, 10) < 7 || true) {
throw new \Exception('I failed randomly!!!!');
... lines 50 - 56

Now, under messenger, let's play with the retry config. Wait... but the async transport is set to a string... are we allowed to include config options under that? No! Well, yes, sort of. As soon as you need to configure a transport beyond just the connection details, you'll need to drop this string onto the next line and assign it to a dsn key. Now we can add retry_strategy, and let's set the delay to 2 seconds instead of 1.

... lines 3 - 5
# https://symfony.com/doc/current/messenger.html#transports
delay: 2000
... lines 12 - 20

Oh, and I also want to mention this service key. If you want to completely control the retry config - maybe even having different retry logic per message - you can create a service that implements RetryStrategyInterface and put its service id - usually its class name - right here.

Anyways, let's see what happens with the longer delay: restart the worker process:

php bin/console messenger:consume -vv

This time, upload just one photo so we can watch it fail over and over again. And... yep! It fails and sends for retry #1... then fails again and sends for retry #2. But check out that delay! 09 to 11 - 2 seconds - then 11 to 15 - a 4 second delay. And... if... we... are... super... patient... yea! Retry #3 starts a full 8 seconds later. Then it's "rejected" - removed from the queue - and lost forever. Tragic!

Retries are great... but I don't like that last part: when the message is eventually lost forever. Change the delay to 500 - it'll make this easier to test.

... lines 3 - 5
# https://symfony.com/doc/current/messenger.html#transports
delay: 500
... lines 12 - 20

Next, let's talk about a special concept called the "failure transport": a better alternative than allowing failed messages to simply... disappear.

Leave a comment!

This tutorial is built with Symfony 4.3, but will work well on Symfony 4.4 or 5.

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // v1.8.0
        "doctrine/doctrine-bundle": "^1.6.10", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^1.3|^2.0", // v2.0.0
        "doctrine/orm": "^2.5.11", // v2.6.3
        "intervention/image": "^2.4", // 2.4.2
        "league/flysystem-bundle": "^1.0", // 1.1.0
        "phpdocumentor/reflection-docblock": "^3.0|^4.0", // 4.3.1
        "sensio/framework-extra-bundle": "^5.3", // v5.3.1
        "symfony/console": "4.3.*", // v4.3.2
        "symfony/dotenv": "4.3.*", // v4.3.2
        "symfony/flex": "^1.9", // v1.18.7
        "symfony/framework-bundle": "4.3.*", // v4.3.2
        "symfony/messenger": "4.3.*", // v4.3.4
        "symfony/property-access": "4.3.*", // v4.3.2
        "symfony/property-info": "4.3.*", // v4.3.2
        "symfony/serializer": "4.3.*", // v4.3.2
        "symfony/validator": "4.3.*", // v4.3.2
        "symfony/webpack-encore-bundle": "^1.5", // v1.6.2
        "symfony/yaml": "4.3.*" // v4.3.2
    "require-dev": {
        "easycorp/easy-log-handler": "^1.0.7", // v1.0.7
        "symfony/debug-bundle": "4.3.*", // v4.3.2
        "symfony/maker-bundle": "^1.0", // v1.12.0
        "symfony/monolog-bundle": "^3.0", // v3.4.0
        "symfony/stopwatch": "4.3.*", // v4.3.2
        "symfony/twig-bundle": "4.3.*", // v4.3.2
        "symfony/var-dumper": "4.3.*", // v4.3.2
        "symfony/web-profiler-bundle": "4.3.*" // v4.3.2