How do I implement retry logic for failed requests in PHP?
Implementing retry logic for failed HTTP requests is crucial for building robust PHP applications that can handle network failures, temporary server errors, and rate limiting. This guide covers various approaches to implement retry mechanisms with exponential backoff, custom error handling, and best practices.
Why Implement Retry Logic?
Retry logic helps your PHP applications handle: - Network timeouts and connection failures - Temporary server errors (5xx status codes) - Rate limiting (429 status codes) - DNS resolution failures - Service unavailability
Basic Retry Implementation with cURL
Here's a simple retry mechanism using PHP's cURL extension:
<?php
function makeRequestWithRetry($url, $maxRetries = 3, $delay = 1) {
$retryCount = 0;
while ($retryCount <= $maxRetries) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'PHP Retry Client/1.0');
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
// Success condition
if ($response !== false && $httpCode >= 200 && $httpCode < 300) {
return [
'success' => true,
'data' => $response,
'http_code' => $httpCode,
'retries' => $retryCount
];
}
// Check if we should retry
if ($retryCount < $maxRetries && shouldRetry($httpCode, $error)) {
$retryCount++;
sleep($delay);
$delay *= 2; // Exponential backoff
continue;
}
// Max retries reached or non-retryable error
return [
'success' => false,
'error' => $error ?: "HTTP $httpCode",
'http_code' => $httpCode,
'retries' => $retryCount
];
}
}
function shouldRetry($httpCode, $error) {
// Retry on network errors
if (!empty($error)) {
return true;
}
// Retry on specific HTTP status codes
$retryableCodes = [408, 429, 500, 502, 503, 504];
return in_array($httpCode, $retryableCodes);
}
// Usage example
$result = makeRequestWithRetry('https://api.example.com/data');
if ($result['success']) {
echo "Request successful after {$result['retries']} retries\n";
echo "Response: " . $result['data'] . "\n";
} else {
echo "Request failed: {$result['error']}\n";
}
?>
Advanced Retry Class with Exponential Backoff
For more sophisticated retry logic, create a dedicated retry class:
<?php
class HttpRetryClient {
private $maxRetries;
private $baseDelay;
private $maxDelay;
private $backoffMultiplier;
private $jitter;
public function __construct(
int $maxRetries = 3,
float $baseDelay = 1.0,
float $maxDelay = 60.0,
float $backoffMultiplier = 2.0,
bool $jitter = true
) {
$this->maxRetries = $maxRetries;
$this->baseDelay = $baseDelay;
$this->maxDelay = $maxDelay;
$this->backoffMultiplier = $backoffMultiplier;
$this->jitter = $jitter;
}
public function request($url, $options = []) {
$retryCount = 0;
$lastError = null;
while ($retryCount <= $this->maxRetries) {
try {
$response = $this->makeRequest($url, $options);
if ($this->isSuccessful($response)) {
return [
'success' => true,
'response' => $response,
'retries' => $retryCount
];
}
if (!$this->shouldRetry($response)) {
break;
}
} catch (Exception $e) {
$lastError = $e;
if (!$this->isRetryableException($e)) {
break;
}
}
if ($retryCount < $this->maxRetries) {
$this->sleep($retryCount);
$retryCount++;
} else {
break;
}
}
return [
'success' => false,
'error' => $lastError ? $lastError->getMessage() : 'Max retries exceeded',
'retries' => $retryCount
];
}
private function makeRequest($url, $options) {
$ch = curl_init();
$defaultOptions = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => 'PHP Retry Client/2.0',
CURLOPT_HEADER => true,
];
curl_setopt_array($ch, array_merge($defaultOptions, $options));
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: $error");
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
return [
'http_code' => $httpCode,
'headers' => substr($response, 0, $headerSize),
'body' => substr($response, $headerSize)
];
}
private function isSuccessful($response) {
return $response['http_code'] >= 200 && $response['http_code'] < 300;
}
private function shouldRetry($response) {
$retryableCodes = [408, 429, 500, 502, 503, 504];
return in_array($response['http_code'], $retryableCodes);
}
private function isRetryableException($exception) {
$message = strtolower($exception->getMessage());
$retryableErrors = [
'timeout',
'connection',
'network',
'dns',
'resolve'
];
foreach ($retryableErrors as $error) {
if (strpos($message, $error) !== false) {
return true;
}
}
return false;
}
private function sleep($retryCount) {
$delay = $this->baseDelay * pow($this->backoffMultiplier, $retryCount);
$delay = min($delay, $this->maxDelay);
if ($this->jitter) {
$delay = $delay * (0.5 + mt_rand() / mt_getrandmax() * 0.5);
}
usleep($delay * 1000000); // Convert to microseconds
}
}
// Usage example
$client = new HttpRetryClient(
maxRetries: 5,
baseDelay: 0.5,
maxDelay: 30.0,
backoffMultiplier: 2.0,
jitter: true
);
$result = $client->request('https://api.example.com/data', [
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['key' => 'value'])
]);
if ($result['success']) {
echo "Success after {$result['retries']} retries\n";
$response = $result['response'];
echo "HTTP Code: {$response['http_code']}\n";
echo "Response Body: {$response['body']}\n";
} else {
echo "Failed: {$result['error']}\n";
}
?>
Using Guzzle HTTP Client with Retry Middleware
For more advanced HTTP handling, use Guzzle with retry middleware:
<?php
require_once 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
class GuzzleRetryClient {
private $client;
public function __construct() {
$stack = HandlerStack::create(new CurlHandler());
// Add retry middleware
$stack->push(Middleware::retry(
$this->retryDecider(),
$this->retryDelay()
));
$this->client = new Client([
'handler' => $stack,
'timeout' => 30,
'connect_timeout' => 10,
]);
}
private function retryDecider() {
return function (
$retries,
Request $request,
Response $response = null,
RequestException $exception = null
) {
// Limit the number of retries
if ($retries >= 5) {
return false;
}
// Retry connection exceptions
if ($exception instanceof RequestException) {
return true;
}
// Retry on server errors
if ($response && $response->getStatusCode() >= 500) {
return true;
}
// Retry on rate limiting
if ($response && $response->getStatusCode() === 429) {
return true;
}
return false;
};
}
private function retryDelay() {
return function ($numberOfRetries) {
return 1000 * pow(2, $numberOfRetries); // Exponential backoff in milliseconds
};
}
public function get($url, $options = []) {
try {
$response = $this->client->get($url, $options);
return [
'success' => true,
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents()
];
} catch (RequestException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'status_code' => $e->getResponse() ? $e->getResponse()->getStatusCode() : null
];
}
}
public function post($url, $data, $options = []) {
try {
$options['json'] = $data;
$response = $this->client->post($url, $options);
return [
'success' => true,
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody()->getContents()
];
} catch (RequestException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'status_code' => $e->getResponse() ? $e->getResponse()->getStatusCode() : null
];
}
}
}
// Usage example
$client = new GuzzleRetryClient();
$result = $client->get('https://api.example.com/data');
if ($result['success']) {
echo "Request successful\n";
echo "Status: {$result['status_code']}\n";
echo "Body: {$result['body']}\n";
} else {
echo "Request failed: {$result['error']}\n";
}
?>
Circuit Breaker Pattern Implementation
For high-traffic applications, implement a circuit breaker pattern to prevent cascading failures:
<?php
class CircuitBreaker {
private $failureThreshold;
private $recoveryTimeout;
private $failureCount = 0;
private $lastFailureTime;
private $state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
public function __construct($failureThreshold = 5, $recoveryTimeout = 60) {
$this->failureThreshold = $failureThreshold;
$this->recoveryTimeout = $recoveryTimeout;
}
public function call(callable $operation) {
if ($this->state === 'OPEN') {
if (time() - $this->lastFailureTime >= $this->recoveryTimeout) {
$this->state = 'HALF_OPEN';
} else {
throw new Exception('Circuit breaker is OPEN');
}
}
try {
$result = $operation();
$this->onSuccess();
return $result;
} catch (Exception $e) {
$this->onFailure();
throw $e;
}
}
private function onSuccess() {
$this->failureCount = 0;
$this->state = 'CLOSED';
}
private function onFailure() {
$this->failureCount++;
$this->lastFailureTime = time();
if ($this->failureCount >= $this->failureThreshold) {
$this->state = 'OPEN';
}
}
public function getState() {
return $this->state;
}
}
// Combined with retry logic
class ResilientHttpClient {
private $retryClient;
private $circuitBreaker;
public function __construct() {
$this->retryClient = new HttpRetryClient();
$this->circuitBreaker = new CircuitBreaker();
}
public function request($url, $options = []) {
try {
return $this->circuitBreaker->call(function() use ($url, $options) {
return $this->retryClient->request($url, $options);
});
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'circuit_breaker_state' => $this->circuitBreaker->getState()
];
}
}
}
?>
Best Practices for Retry Logic
1. Use Exponential Backoff with Jitter
Prevent thundering herd problems by adding randomness to delay calculations.
2. Set Reasonable Limits
// Good retry configuration
$config = [
'max_retries' => 3,
'base_delay' => 1.0,
'max_delay' => 60.0,
'timeout' => 30
];
3. Handle Rate Limiting Properly
private function handleRateLimit($response) {
$headers = $response['headers'];
if (isset($headers['Retry-After'])) {
$retryAfter = (int) $headers['Retry-After'];
sleep($retryAfter);
return true;
}
return false;
}
4. Log Retry Attempts
private function logRetry($url, $attempt, $error) {
error_log("Retry attempt $attempt for $url: $error");
}
5. Monitor and Alert
Track retry patterns to identify systemic issues:
class RetryMetrics {
public static function recordRetry($service, $attempts, $success) {
// Send metrics to monitoring system
// Example: StatsD, Prometheus, etc.
}
}
Integration with Web Scraping
When implementing retry logic for web scraping projects, similar to how you might handle errors in Puppeteer, consider these additional factors:
function scrapeWithRetry($url, $selector) {
$client = new HttpRetryClient();
$result = $client->request($url, [
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; WebScraper/1.0)',
CURLOPT_HTTPHEADER => [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.5',
'Accept-Encoding: gzip, deflate'
]
]);
if ($result['success']) {
$dom = new DOMDocument();
@$dom->loadHTML($result['response']['body']);
$xpath = new DOMXPath($dom);
$elements = $xpath->query($selector);
return array_map(function($element) {
return $element->textContent;
}, iterator_to_array($elements));
}
return [];
}
Command Line Testing
Test your retry logic with command line tools:
# Test with curl to simulate timeouts
curl --max-time 1 --retry 3 --retry-delay 2 https://httpbin.org/delay/5
# Test with PHP script
php -r "
include 'retry_client.php';
\$client = new HttpRetryClient();
\$result = \$client->request('https://httpbin.org/status/500');
var_dump(\$result);
"
Monitoring and Debugging
Add comprehensive logging and monitoring to your retry implementation:
class RetryLogger {
public static function logAttempt($url, $attempt, $httpCode, $error = null) {
$timestamp = date('Y-m-d H:i:s');
$message = "[$timestamp] Retry $attempt for $url - HTTP $httpCode";
if ($error) {
$message .= " - Error: $error";
}
error_log($message);
}
public static function logSuccess($url, $totalAttempts, $duration) {
$timestamp = date('Y-m-d H:i:s');
$message = "[$timestamp] Success for $url after $totalAttempts attempts in {$duration}ms";
error_log($message);
}
public static function logFailure($url, $totalAttempts, $lastError) {
$timestamp = date('Y-m-d H:i:s');
$message = "[$timestamp] Final failure for $url after $totalAttempts attempts - $lastError";
error_log($message);
}
}
Conclusion
Implementing robust retry logic in PHP requires careful consideration of failure modes, backoff strategies, and monitoring. Whether you're building a simple web scraper or a complex API client, proper retry mechanisms ensure your applications remain resilient in the face of network failures and temporary service outages.
The examples provided here offer various approaches from basic retry loops to sophisticated circuit breaker patterns. Choose the implementation that best fits your application's requirements and traffic patterns. Remember to always monitor your retry behavior and adjust parameters based on real-world performance data.