PHP cookbook · Intermediate

Call external APIs with timeouts and retries in PHP

Call a flaky third-party API without hanging PHP-FPM workers or amplifying outages.

Approach

Use Guzzle or Symfony HttpClient with explicit connect and total timeouts, retry only idempotent requests on transient failures (connection errors, 429, 502–504) with exponential backoff and jitter, honor Retry-After, and cap attempts. Map failures to domain exceptions.

Solution

<?php
declare(strict_types=1);

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

$stack = HandlerStack::create();
$stack->push(Middleware::retry(
    function (int $retries, RequestInterface $req, ?ResponseInterface $res, ?Throwable $e): bool {
        if ($retries >= 3 || !in_array($req->getMethod(), ['GET', 'HEAD', 'PUT', 'DELETE'], true)) {
            return false;
        }
        return $e instanceof GuzzleHttp\Exception\ConnectException
            || in_array($res?->getStatusCode(), [429, 502, 503, 504], true);
    },
    function (int $retries, ?ResponseInterface $res): int {
        $retryAfter = (int) ($res?->getHeaderLine('Retry-After') ?: 0);
        return $retryAfter > 0 ? $retryAfter * 1000 : (int) (100 * 2 ** $retries + random_int(0, 100));   // ms
    },
));

$client = new Client([
    'base_uri' => 'https://api.example.com/',
    'handler' => $stack,
    'connect_timeout' => 2.0,
    'timeout' => 5.0,
    'headers' => ['Accept' => 'application/json'],
]);

$rates = json_decode((string) $client->get('rates/USD')->getBody(), true, flags: JSON_THROW_ON_ERROR);

Pitfalls

  • Default Guzzle timeout of 0 (wait forever).
  • Retrying POSTs that aren't idempotent (double charges).
  • Retries multiplying load during an outage — add a circuit breaker for critical dependencies.

Variations

  • Symfony HttpClient's RetryableHttpClient with GenericRetryStrategy.
  • Laravel HTTP client: Http::timeout(5)->retry(3, 200)->get(...).

Further reading: Guzzle: Request options

Reference: https://docs.guzzlephp.org/en/stable/request-options.html#timeout

More PHP recipes