DEV Community

Cover image for How to handle API rate limits and HTTP 429 errors in an easy and reliable way
Roberto B.
Roberto B.

Posted on

How to handle API rate limits and HTTP 429 errors in an easy and reliable way

API rate limits and HTTP 429 (Too Many Requests) errors are common challenges when building applications that integrate with third-party APIs, as they require multiple requests within a short time window.

A typical first approach is to handle 429 responses directly in the application logic by adding custom retry and delay mechanisms; however, this often results in complex, fragile, and difficult-to-maintain code.

When integrating APIs using Symfony HTTP Client, there is no need to reinvent the wheel. Symfony provides a robust, configurable, and production-ready solution through the RetryableHttpClient, allowing you to handle API rate limits automatically and reliably while keeping your code clean and maintainable.

Even if you are not using the Symfony Framework and are working with a standalone PHP script, you can still rely on the Symfony HTTP Client component.

Rate limits and 429 HTTP status/errors

When working with third-party APIs, you may occasionally encounter an HTTP 429 – Too Many Requests response. This status code indicates that the client has exceeded the server’s rate limit, typically the number of allowed requests within a specific period (for example, 10 requests per second).

When a 429 is returned, the API/service is telling the client:

“You are sending requests too fast. Please slow down and try again later.”

To successfully complete the request, you should retry the same API call after waiting the amount of time recommended by the server or, if not provided, after a sensible delay such as one second.

Symfony’s HTTP Client component includes built-in support for retrying failed requests through the RetryableHttpClient class. This allows developers to automatically retry requests that fail due to rate limiting, network issues, or other recoverable errors.

In this article, we will focus on configuring retry mechanisms in Symfony when an HTTP 429 response is received, while keeping in mind that the same approach can also be applied to other recoverable HTTP errors, such as 500-level responses.

Why implement retries for HTTP 429?

APIs enforce rate limits to protect their infrastructure and ensure fair usage among clients. Exceeding these limits results in a temporary block, which is why a 429 response is returned.

Automatically retrying a failed request is helpful because:

  • The request can succeed shortly after the rate limit resets.
  • It improves the resiliency of your application.
  • It avoids manually implementing sleep/delay logic.

Many APIs also include a Retry-After HTTP header indicating how long the client should wait. Symfony’s retry system can detect this and adjust the delay accordingly.

Using RetryableHttpClient in Symfony

Symfony provides the RetryableHttpClient wrapper, which adds automatic retry behavior to an existing HTTP client.

To enable retries, you must configure a Retry Strategy, which defines:

  • how many times to retry,
  • which HTTP status codes trigger a retry (e.g., 429),
  • how long to wait between attempts.

Below is a practical example.

Example: retry failed requests after a 429 status

The following example shows how to configure automatic retries for API requests that return an HTTP 429 (Too Many Requests) response using Symfony HTTP Client.
This approach works in both full Symfony applications and standalone PHP scripts, without requiring the Symfony Framework itself.

Step 1: Install the Symfony HTTP Client library

Even if you are not using a Symfony application, you can install the Symfony HTTP Client component via Composer:

composer require symfony/http-client
Enter fullscreen mode Exit fullscreen mode

This installs only the HTTP Client component and its dependencies.
You do not need to install or use the Symfony Framework to take advantage of its retry and networking features.

Step 2: Import required classes

require "./vendor/autoload.php";

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use Symfony\Component\HttpClient\RetryableHttpClient;
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure a retry strategy

Here we configure a strategy that retries when receiving HTTP 429 responses:

$retryStrategy = new GenericRetryStrategy(
    statusCodes: [429],   // Retry on HTTP 429 Too Many Requests
    delayMs: 1000,        // Wait 1 second between retries
    multiplier: 1.0,      // Do not increase the delay
    maxDelayMs: 2000,     // Maximum delay per retry
);
Enter fullscreen mode Exit fullscreen mode

This strategy says: "If you receive a 429, retry waiting 1 second between attempts."

Step 4: Wrap your HTTP client

$client = HttpClient::create();

$retryingClient = new RetryableHttpClient(
    client: $client,
    strategy: $retryStrategy,
    maxRetries: 3,
);
Enter fullscreen mode Exit fullscreen mode

Step 5: Make the HTTP request and validate the final response

$response = $retryingClient->request(
    "GET",
    "https://dummyjson.com/products/search?q=phone",
);
Enter fullscreen mode Exit fullscreen mode

When using the RetryableHttpClient, Symfony will automatically retry the request if a retryable error occurs, such as an HTTP 429 (Too Many Requests), according to the configured retry strategy.

However, even after all retry attempts are exhausted, the request may still fail. This can happen, for example, if:

  • the API continues to return 429 responses beyond the maximum number of retries,
  • the server responds with a different non-retryable error (such as 400 or 401),
  • a network or timeout error occurs.

For this reason, it is crucial to always validate the final response before using its data.

A safer approach is to check the HTTP status code explicitly:

$statusCode = $response->getStatusCode();

if ($statusCode === 200) {
    $data = $response->toArray();
    print_r($data);
} else {
    // Handle the error (logging, fallback logic, exception, etc.)
    echo $response->getStatusCode();
}
Enter fullscreen mode Exit fullscreen mode

Handling transport exceptions

In addition to HTTP error responses (such as 429 or 500), API requests may fail due to transport-level errors. These errors occur before a valid HTTP response is received and are typically caused by:

  • network connectivity issues,
  • DNS resolution failures,
  • timeouts,
  • SSL/TLS errors.

When this happens, Symfony HTTP Client throws transport exceptions, which must be handled explicitly to prevent unexpected application failures.

Catching Transport Exceptions Safely

Symfony HTTP Client throws exceptions that implement the TransportExceptionInterface.
You should wrap your request logic in a try/catch block to handle these cases gracefully.

use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

try {
    $response = $retryingClient->request(
        "GET",
        "https://dummyjson.com/products/search?q=phone",
    );
    $statusCode = $response->getStatusCode();

    if ($statusCode === 200) {
        $data = $response->toArray();
        print_r($data);
    } else {
        // Handle the error (logging, fallback logic, exception, etc.)
        echo $response->getStatusCode();
    }
} catch (TransportExceptionInterface $e) {
    // Handle transport-level errors (network, timeout, DNS, etc.)
    // e.g. log the error, notify monitoring systems, or retry later
    echo $e->getMessage();
}
Enter fullscreen mode Exit fullscreen mode

Why validating responses is important

Even with automatic retries enabled:

  • a request can still fail due to network issues,
  • retries only apply when a response is actually received,
  • transport errors bypass HTTP status codes entirely.

By explicitly catching transport exceptions, you ensure that your application:

  • remains stable under adverse network conditions,
  • fails gracefully instead of crashing,
  • provides meaningful error handling and observability.

Key takeaway about making the HTTP request

A reliable API integration must handle both HTTP-level errors and transport-level exceptions.

Combining RetryableHttpClient, response status validation, and proper exception handling gives you a robust, production-ready solution for interacting with rate-limited APIs.

Handling Retry-After Header

Many APIs send a Retry-After header with a value in seconds.

Symfony’s retry system will automatically prioritize the server-provided delay when available.

Example server header:

HTTP/1.1 429 Too Many Requests
Retry-After: 2
Enter fullscreen mode Exit fullscreen mode

In this case, Symfony will wait 2 seconds, even if your strategy says 1 second.

Conclusion

Automatic retries are essential when interacting with rate-limited APIs. Symfony’s RetryableHttpClient makes this process easy and robust by:

  • automatically handling 429 (Too Many Requests),
  • supporting server-provided retry delays,
  • providing configurable retry strategies.

By wrapping your HTTP client with a retry strategy, your application becomes more resilient and better equipped to deal with temporary API throttling.

References

Top comments (0)