title: Retry Handling description: Learn how to configure automatic retry behavior for failed HTTP requests#
Retry Handling#
The Fetch PHP package includes built-in retry functionality to handle transient failures gracefully. This guide explains how to configure and use the retry mechanism.
Basic Retry Configuration#
You can configure retries using the retry() method on the ClientHandler:
use Fetch\Http\ClientHandler;
$response = ClientHandler::create()
->retry(3, 100) // Retry up to 3 times with initial delay of 100ms
->get('https://api.example.com/unstable-endpoint');
Using helper functions:
$response = fetch('https://api.example.com/unstable-endpoint', [
'retries' => 3, // Retry up to 3 times
'retry_delay' => 100 // Initial delay of 100ms
]);
How Retry Works#
When a request fails due to a retryable error (network issues or certain HTTP status codes), the package will:
- Wait for a specified delay
- Apply exponential backoff with jitter (randomness)
- Retry the request
- Repeat until success or the maximum retry count is reached
The delay increases exponentially with each retry attempt:
- First retry: Initial delay (e.g., 100ms)
- Second retry: ~2x initial delay + jitter
- Third retry: ~4x initial delay + jitter
- And so on...
The jitter (random variation) helps prevent multiple clients from retrying simultaneously, which can worsen outages.
Using Type-Safe Enums with Retries#
You can use the Status enum for more type-safe retry configuration:
use Fetch\Http\ClientHandler;
use Fetch\Enum\Status;
$response = ClientHandler::create()
->retry(3, 100)
->retryStatusCodes([
Status::TOO_MANY_REQUESTS->value,
Status::SERVICE_UNAVAILABLE->value,
Status::GATEWAY_TIMEOUT->value
])
->get('https://api.example.com/unstable-endpoint');
Customizing Retryable Status Codes#
By default, the client retries on these HTTP status codes:
- 408 (Request Timeout)
- 429 (Too Many Requests)
- 500, 502, 503, 504 (Server Errors)
- And several other common error codes
You can customize which status codes should trigger retries:
$client = ClientHandler::create()
->retry(3, 100)
->retryStatusCodes([429, 503, 504]); // Only retry on these status codes
$response = $client->get('https://api.example.com/unstable-endpoint');
Customizing Retryable Exceptions#
By default, the client retries on network-related exceptions like ConnectException. You can customize which exception types should trigger retries:
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
$client = ClientHandler::create()
->retry(3, 100)
->retryExceptions([
ConnectException::class,
RequestException::class
]);
$response = $client->get('https://api.example.com/unstable-endpoint');
Checking Retry Configuration#
You can check the current retry configuration:
$client = ClientHandler::create()->retry(3, 200);
$maxRetries = $client->getMaxRetries(); // 3
$retryDelay = $client->getRetryDelay(); // 200
$statusCodes = $client->getRetryableStatusCodes(); // Array of status codes
$exceptions = $client->getRetryableExceptions(); // Array of exception classes
Global Retry Configuration#
You can set up global retry settings that apply to all requests:
// Configure global retry settings
fetch_client([
'retries' => 3,
'retry_delay' => 100
]);
// All requests will now use these retry settings
$response = fetch('https://api.example.com/users');
Logging Retries#
If you've set up a logger, retry attempts will be automatically logged:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fetch\Http\ClientHandler;
// Create a logger
$logger = new Logger('http');
$logger->pushHandler(new StreamHandler('logs/http.log', Logger::INFO));
// Create a client with logging and retries
$client = ClientHandler::create();
$client->setLogger($logger);
$client->retry(3, 100);
// Send a request that might require retries
$response = $client->get('https://api.example.com/unstable-endpoint');
// Retry attempts will be logged to logs/http.log
A typical retry log entry looks like:
[2023-09-15 14:30:12] http.INFO: Retrying request {"attempt":1,"max_attempts":3,"uri":"https://api.example.com/unstable-endpoint","method":"GET","error":"Connection timed out","code":28}
Asynchronous Retries#
Retries also work with asynchronous requests:
use function async;
use function await;
use function retry;
// Retry asynchronous operations
$result = await(retry(
function() {
return async(function() {
return fetch('https://api.example.com/unstable-endpoint');
});
},
3, // max attempts
function($attempt) {
// Exponential backoff strategy
return min(pow(2, $attempt) * 100, 1000);
}
));
// Process the result
$data = $result->json();
Real-World Examples#
Handling Rate Limits#
APIs often implement rate limiting. You can configure your client to automatically retry when hitting rate limits:
use Fetch\Enum\Status;
$client = ClientHandler::create()
->retry(3, 1000) // Longer initial delay for rate limits
->retryStatusCodes([Status::TOO_MANY_REQUESTS->value]) // Only retry on Too Many Requests
->get('https://api.example.com/rate-limited-endpoint');
Handling Network Instability#
For unreliable network connections:
$client = ClientHandler::create()
->retry(5, 200) // More retries with moderate delay
// Using default retryable status codes and exceptions
->get('https://api.example.com/endpoint');
Handling Server Maintenance#
For APIs that might be temporarily down for maintenance:
use Fetch\Enum\Status;
$client = ClientHandler::create()
->retry(10, 5000) // Many retries with long delay (5 seconds)
->retryStatusCodes([Status::SERVICE_UNAVAILABLE->value]) // Service Unavailable
->get('https://api.example.com/endpoint');
Combining Retry with Timeout#
You can combine retry logic with timeout settings:
$client = ClientHandler::create()
->timeout(5) // 5 second timeout for each attempt
->retry(3, 100) // 3 retries with 100ms initial delay
->get('https://api.example.com/endpoint');
Implementing Advanced Retry Logic#
For more complex scenarios, you can implement custom retry logic:
use Fetch\Http\ClientHandler;
use Fetch\Http\Response;
use GuzzleHttp\Exception\RequestException;
use Fetch\Enum\Status;
function makeRequestWithCustomRetry(string $url, int $maxAttempts = 3): Response {
$attempt = 0;
while (true) {
try {
$client = ClientHandler::create();
$response = $client->get($url);
// Check if we got a success response
if ($response->successful()) {
return $response;
}
// Handle specific status codes
if ($response->statusEnum() === Status::TOO_MANY_REQUESTS) {
// Get retry-after header if available
$retryAfter = $response->header('Retry-After');
$delay = $retryAfter ? (int) $retryAfter * 1000 : 1000;
} else {
// Otherwise use exponential backoff
$delay = 100 * (2 ** $attempt);
}
// Add some jitter (±20%)
$jitter = mt_rand(-20, 20) / 100;
$delay = (int) ($delay * (1 + $jitter));
$attempt++;
// Check if we've exceeded max attempts
if ($attempt >= $maxAttempts) {
return $response; // Return the last response
}
// Wait before retrying
usleep($delay * 1000); // Convert ms to μs
} catch (RequestException $e) {
$attempt++;
// Check if we've exceeded max attempts
if ($attempt >= $maxAttempts) {
throw $e; // Rethrow the last exception
}
// Wait before retrying
$delay = 100 * (2 ** $attempt);
usleep($delay * 1000);
}
}
}
// Use the custom retry function
$response = makeRequestWithCustomRetry('https://api.example.com/users');
Monitoring Retry Activity#
To monitor retry activity, you can combine logging with a custom callback:
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Fetch\Http\ClientHandler;
// Create a logger
$logger = new Logger('retry');
$logger->pushHandler(new StreamHandler('logs/retry.log', Logger::INFO));
// Create a client with the logger
$client = ClientHandler::create();
$client->setLogger($logger);
$client->retry(3, 100);
// Make the request
$response = $client->get('https://api.example.com/unstable-endpoint');
// After the request completes, you can get debug info
$debugInfo = $client->debug();
echo "Request required " . $debugInfo['retries'] . " retries\n";
Best Practices#
-
Use Type-Safe Enums: Leverage the Status enum for clearer and safer code when configuring retryable status codes.
-
Start with Conservative Settings: Begin with a small number of retries (2-3) and moderate delays (100-200ms) and adjust based on your needs.
-
Be Mindful of Server Load: Excessive retries can amplify problems during outages. Be respectful of the services you're calling.
-
Use Appropriate Timeout Values: Set reasonable timeouts in conjunction with retries to avoid long-running requests.
-
Limit Retryable Status Codes: Only retry on status codes that indicate transient issues. Don't retry on client errors like 400, 401, or 404.
-
Monitor Retry Activity: Log retry attempts to identify recurring issues with specific endpoints.
-
Consider Retry-After Headers: For rate limiting (429), respect the Retry-After header if provided by the server.
-
Add Jitter: The built-in retry mechanism includes jitter, which helps prevent "thundering herd" problems.
-
Combine with Logging: Always add logging when using retries to track and debug retry patterns.
-
Use Async Retries for Parallel Operations: When working with async code, use the retry function for better integration with the async/await pattern.
Next Steps#
- Learn about Error Handling for comprehensive error management
- Explore Logging for monitoring request and retry activity
- See Authentication for handling authentication errors and retries
- Check out Asynchronous Requests for integrating retries with async operations