Symfonyのretry_failed optionsを活用すると、APIリクエストが失敗した際に指数バックオフアルゴリズムによる再試行が簡単に実装できます。

本記事では、その設定方法と検証結果を紹介します :saluting_face:

指数バックオフアルゴリズム (Exponential Backoff Algorithm)とは

ネットワークやAPIリトライなどで使われる「待ち時間を指数的に増やして再試行する仕組み」です。

例えば初回 1秒 → 2秒 → 4秒 → 8秒 … のように、待機時間を指数関数的に増加させてリトライ処理をします。

過去記事「リトライ処理時の指数バックオフアルゴリズムを AWS Step Functions で実装」も参考になるので読んでみてください。
https://tech.quartetcom.co.jp/2024/10/29/exponential-backoff-step-functions/

Symfonyのretry_failed optionsの設定方法

config/packages/framework.yamlを以下のように設定します。
※Symfony 5.2以降で利用可能です。

//  config/packages/framework.yaml

framework:
    http_client:
        default_options:
            retry_failed:
                enabled: true
                max_retries: 5 # 最大リトライ回数
                delay: 1000    # 初期遅延 (ミリ秒) → 1s
                multiplier: 2  # 指数の倍率(2 なら 1s,2s,4s,8s...)
                max_delay: 64000 # 最大待機(ミリ秒)→ Memorystoreの推奨に合わせ64s上限など
                jitter: 0.1    # ジッター(0.0〜1.0、ここでは10%の揺らし)。同じタイミングで大量のリトライが発生しないようにランダムなばらつきをもたせる
                http_codes: [ 423, 425, 429, 500, 502, 503, 504, 507, 510 ] # リトライ対象のHTTPコード

特定API使用時のみリトライする場合は、scoped_clientsを使います。

framework:
    http_client:
-        default_options:
+        scoped_clients:
+            api_time.client:
+               base_uri: 'https://127.0.0.1:8000/api/time'
                    retry_failed:
                        enabled: true
                        max_retries: 5
                        ...
//src/Controller/RequestController.php

...
use Symfony\Contracts\HttpClient\HttpClientInterface:

final class RequestController extends AbstractController
{
    #[Route('/request', name: 'app_request')]
    public function index(
        #[Autowire(service: 'api_time.client')]
        HttpClientInterface $httpClient
    ): Response
    {
        //APIにアクセスする
        $response = $httpClient->request('GET', 'https://127.0.0.1:8000/api/time');
        if ($response->getStatusCode() !== 200) {
            return $this->render('request/error.html.twig', [
                'message' => 'API request failed with status code: ' . $response->getStatusCode(),
            ]);
        }

        $content = $response->toArray();
        return $this->render('request/index.html.twig', [
            'time' => $content['time'],
        ]);
    }
}

参考: https://symfony.com/doc/current/reference/configuration/framework.html#http-client

本当に指数関数的にリトライしているか検証

以下の手順で検証を行いました。

  1. HTTP 429エラー(2リクエスト/10秒)を意図的に発生させるAPIを作成
  2. 1で作成したAPIにリクエストをするクライアントにSymfonyのretry_failedオプションを設定
  3. リトライ処理が実際に行われているか確認

1. HTTP 429エラー(2リクエスト/10秒)を意図的に発生させるAPIを作成

composer create-project symfony/skeleton:"7.3.x" rate-limitter-example
cd rate-limitter-example/
composer require webapp
composer require symfony/rate-limiter
bin/console make:controlle ApiTimeController
// src/Controller/ShowTimeController.php

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
use Symfony\Component\Routing\Attribute\Route;

final class ApiTimeController extends AbstractController
{
    #[Route('/api/time', name: 'app_api_time')]
    public function index(Request $request, RateLimiterFactoryInterface $apiTimeLimiter): Response
    {
        $limiter = $apiTimeLimiter->create($request->getClientIp());

        // consume(1)の1は消費するトークン数
        if (false === $limiter->consume(1)->isAccepted()) {
            throw new TooManyRequestsHttpException(null, 'Too Many Requests');
        }

        return $this->json([
            'time' => (new \DateTime())->format('Y-m-d H:i:s'),
        ]);
    }
}    
// config/packages/rate_limiter.yaml

framework:
    rate_limiter:
        api_time:
            # use 'sliding_window' if you prefer that policy
            policy: 'fixed_window'
            limit: 2
            interval: '10 second'

2. 1で作成したAPIを使用するクライアントにSymfonyのretry_failedオプションを設定

composer create-project symfony/skeleton:"7.3.x" clear-late-limiter
cd clear-late-limiter/
composer require webapp
bin/console make:controlle RequestController
<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class RequestController extends AbstractController
{
    #[Route('/request', name: 'app_request')]
    public function index(
        HttpClientInterface $httpClient
    ): Response
    {
        //APIにアクセスする
        $response = $httpClient->request('GET', 'https://127.0.0.1:8000/api/time');
        if ($response->getStatusCode() !== 200) {
            return $this->render('request/error.html.twig', [
                'message' => 'API request failed with status code: ' . $response->getStatusCode(),
            ]);
        }

        $content = $response->toArray();
        return $this->render('request/index.html.twig', [
            'time' => $content['time'],
        ]);
    }
}
//  config/packages/framework.yaml

framework:
    http_client:
        default_options:
            retry_failed:
                enabled: true
                max_retries: 5 # 最大リトライ回数
                delay: 1000    # 初期遅延 (ミリ秒) → 1s
                multiplier: 2  # 指数の倍率(2 なら 1s,2s,4s,8s...)
                max_delay: 64000 # 最大待機(ミリ秒)→ Memorystoreの推奨に合わせ64s上限など
                jitter: 0.0    # ジッター(0.0〜1.0、ここでは10%の揺らし)。同じタイミングで大量のリトライが発生しないようにランダムなばらつきをもたせる
                http_codes: [ 423, 425, 429, 500, 502, 503, 504, 507, 510 ] # リトライ対象のHTTPコード

上記の設定で指数関数的にリトライするように設定します。 今回は検証結果がわかりやすいように、jitter: 0.0とします。

3. リトライ処理が実際に行われているか確認

symfony server:startで各サーバを立てます。以下

とします。

https://127.0.0.1:8001/request に短時間に連続してアクセスしてみます。

その後、クライアント側のProfiler > HTTP Client > Responseを確認すると以下のことがわかります。

  • "retry_count" => 4より、4回リトライし、5回目で成功している
  • "retries"より、各リトライが1秒後 → 2秒後 → 4秒後 → 8秒後と指数関数的に試行されている
[["info" => [▼
    ...
    "debug" => """
      ...
      * Request completely sent off
      < HTTP/2 200 
      < cache-control: no-cache, private
      < content-type: application/json
      < date: Mon, 22 Sep 2025 01:37:56 GMT
      ...
  ]
  "retry_count" => 4
  "response_headers" => [▶]
  "response_json" => [▶]
  "retries" => [▼
    [▼
      ...
      "debug" => """
        ...
        * Request completely sent off
        < HTTP/2 429 
        < cache-control: no-cache, private
        < content-type: text/html; charset=UTF-8
        < date: Mon, 22 Sep 2025 01:37:41 GMT
        ...
    ]
    [▼
      ...
      "debug" => """
        ...
        * Request completely sent off
        < HTTP/2 429 
        < cache-control: no-cache, private
        < content-type: text/html; charset=UTF-8
        < date: Mon, 22 Sep 2025 01:37:42 GMT
        ...
    ]
    [▼
      ...
      "debug" => """
        ...        
        * Request completely sent off
        < HTTP/2 429 
        < cache-control: no-cache, private
        < content-type: text/html; charset=UTF-8
        < date: Mon, 22 Sep 2025 01:37:44 GMT
        ...
    ]
    [▼
      ...
      "debug" => """
        ...
        * Request completely sent off
        < HTTP/2 429 
        < cache-control: no-cache, private
        < content-type: text/html; charset=UTF-8
        < date: Mon, 22 Sep 2025 01:37:48 GMT
        ...
    ]
  ]
]

まとめ

今回の検証では、2リクエスト/10秒という制限のあるAPIに対して、delay: 1000(=1秒)・multiplier: 2に設定したところ、リトライが頻繁に発生し、設定した最大リトライ回数(max_retries)に達してしまうケースがありました。

このため、API側のレート制限に合わせて、delayやmultiplier、max_retriesなどのパラメータを適切に調整することが重要です :warning:

Symfonyのretry_failed optionsを活用すれば、指数バックオフアルゴリズムによるリトライ機能を簡単に設定できます!ぜひ皆さんのプロジェクトでも取り入れてみてください :heart_hands: