Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Retry a failed PHP cURL request in application code: check whether curl_exec() returned false, save the cURL error details while the handle is open, and retry only within a finite policy. Treat HTTP status codes separately. A 404 response, for example, is not a cURL transfer failure by default. Before retrying a request that can change server state, make sure repeating it is safe.

First, distinguish a transfer failure from an HTTP error

With CURLOPT_RETURNTRANSFER enabled, curl_exec() returns the response body when the transfer succeeds and false when the transfer fails. A successful transfer can still return an HTTP error response: by default, cURL does not treat a status such as 404 as a transfer failure. That means an if (!$body) check is not enough to make the decision correctly. Check strictly for false, then inspect the HTTP status independently.

Result What to inspect Retry decision
curl_exec() returns false curl_errno() and curl_error(), read before closing the handle Consider retrying if the failure is transient, the request is safe to repeat, and the retry budget remains.
curl_exec() returns a body HTTP status from curl_getinfo() Apply the endpoint’s status policy. An HTTP response is not automatically a cURL failure.

curl_errno() returns zero when there was no cURL error; curl_error() returns an empty string when there was none. The error number is useful for programmatic decisions, while the message helps with logging and diagnosis. Do not discard the handle before collecting them.

A bounded PHP example for a GET-like request

This example retries transfer failures only. It makes three total attempts at most, gives each attempt a five-second connection timeout and a 15-second total timeout, and uses a short increasing delay between attempts. Those counts, timeouts, and delays are illustrative policy choices, not universal cURL recommendations. The example accepts only a 2xx HTTP response; other status codes throw an exception without being retried.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php
function getWithRetries(string $url, int $maxAttempts = 3): string
{
    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 15,
        ]);

        $body = curl_exec($ch);
        if ($body !== false) {
            $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            curl_close($ch);

            // Decide separately whether this HTTP status is acceptable.
            if ($status >= 200 && $status < 300) {
                return $body;
            }
            throw new RuntimeException("HTTP status {$status}");
        }

        $errno = curl_errno($ch);
        $error = curl_error($ch);
        curl_close($ch);

        if ($attempt === $maxAttempts) {
            throw new RuntimeException("cURL error {$errno}: {$error}");
        }

        // Example bounded delay; tune for the caller's latency budget.
        usleep(100_000 * $attempt);
    }

    throw new RuntimeException('Request attempts exhausted');
}

try {
    $body = getWithRetries('https://example.com/data');
    // Use or decode $body here.
} catch (RuntimeException $e) {
    // Log the failure and handle it at the appropriate application boundary.
    error_log($e->getMessage());
}

Replace the example URL with the endpoint you intend to call. The handle is closed both after a successful transfer and after a transfer failure, and the error number and text are saved before closing. A status rejection is thrown after the handle has been closed. In production code, catch failures at the layer that can make a useful decision: for example, whether to report an upstream error, return a fallback, or surface a failure to the caller.

What this example does not do

  • It does not retry HTTP responses such as 429 or 503. Add status retries only if the upstream endpoint’s behavior and your application’s requirements justify them.
  • It does not enforce one wall-clock deadline across the entire retry operation. The per-attempt timeout limits one transfer; several attempts and delays can take longer than one timeout.
  • It does not add jitter, parse Retry-After, classify particular cURL error numbers, or handle multi-handle transfers. These require policy tailored to the service and caller’s latency budget.
  • It is for a GET-like operation, not a blanket rule for POST, payment, or other state-changing requests.

Decide which failures are eligible for another attempt

Transfer failures

A false result tells you the transfer failed, not that another attempt will fix it. Your application should decide whether a failure is plausibly transient and worth retrying. A persistent configuration problem, for example, will not be solved by sending the same request repeatedly. Keep the policy finite and preserve the last error for the caller or logs; do not retry forever or turn all errors into an unexplained empty response.

For simple policies, retrying all transfer failures a small, bounded number of times may be acceptable for a safe read request. For a more sensitive integration, classify failures deliberately and retry only those the upstream service or your operational requirements identify as appropriate. The PHP cURL references do not define a universal list of retryable errors.

HTTP responses

When a response body is returned, inspect its status using curl_getinfo($ch, CURLINFO_RESPONSE_CODE) before deciding what the application should do. The sample treats every non-2xx response as an exception but does not retry it. That is an example of explicit handling, not a universal status policy. A 404 can mean the requested resource does not exist; retrying it without a reason is unlikely to help. Other codes may call for endpoint-specific handling, but decide that based on the service contract rather than assuming every 4xx or 5xx response should be retried.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If you enable CURLOPT_FAILONERROR

CURLOPT_FAILONERROR changes cURL’s handling of HTTP response codes at or above 400. If you enable it, account for that behavior in your error reporting and retry logic: a result that would otherwise be handled as an HTTP response may be surfaced at the cURL layer. If your application needs to distinguish transport failures from HTTP statuses clearly, leaving this option off and checking the status explicitly is a straightforward approach.

Make sure repeating the request is safe

A retry sends another request to the server. For a read-only GET-like operation, repeating the request is often the intended behavior, but confirm the endpoint’s semantics rather than relying on the method name alone. A request that creates, charges, deletes, or otherwise changes state can succeed at the server even if the client fails to receive the response. Retrying it could repeat the side effect.

Before retrying a state-changing request, establish an idempotency strategy with the API or application. If the endpoint does not offer a way to safely identify duplicate attempts, do not apply the sample unchanged. Decide how to reconcile an uncertain outcome instead of assuming that a transfer error proves the server did nothing.

Set timeouts and an overall retry budget

Set both a connection timeout and a total timeout for each transfer. CURLOPT_CONNECTTIMEOUT bounds time spent establishing the connection; CURLOPT_TIMEOUT bounds the complete transfer. The connection time is included in the total timeout, so the total does not begin only after connection setup finishes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A per-attempt timeout is not the same as a deadline for the whole operation. If your caller must finish by a particular time, calculate a total retry budget that includes all attempts and wait periods, and stop when that budget is exhausted. Otherwise, three attempts with a 15-second total timeout can consume materially more time than a caller with a short response deadline can tolerate. Choose attempt limits and delays to fit that caller’s latency budget.

Backoff, jitter, and service guidance

The sample’s usleep(100_000 * $attempt) is only a small illustrative increasing delay. It is not a recommended universal schedule. An integration may need a different backoff, jitter to avoid synchronized retry bursts, or respect for a service-provided retry delay. The official PHP and libcurl mechanics do not prescribe an attempt count, backoff algorithm, jitter policy, or retryable status list. Use the upstream API’s guidance and your own deadline and load constraints.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshoot common retry problems

  • The code does not retry on a 404. By default, a 404 is an HTTP response, not a transfer failure. Inspect the response status and implement a separate status policy only if appropriate for the endpoint.
  • The error number or message is empty. You may be reading diagnostics after a successful transfer, or after closing/discarding the handle. Read curl_errno() and curl_error() immediately when curl_exec() returns false, before closing it.
  • The caller still times out despite per-attempt timeouts. The timeout applies to one transfer, not the entire loop. Add an overall deadline that includes delays and subsequent attempts, and stop when it is reached.
  • A request runs twice or creates duplicate data. A retry may repeat a server-side action even if the first response was lost. Remove the blanket retry for that operation until safe repeatability or an idempotency strategy is established.
  • HTTP statuses appear as cURL failures. Check whether CURLOPT_FAILONERROR is enabled. Its behavior for status codes at or above 400 needs to be included in your status and error policy.
  • Multi-handle diagnostics do not match the single-handle example. For multi-handle transfers, use the individual result returned by curl_multi_info_read(); do not assume the single-handle curl_errno() flow describes each transfer.

Or skip the browser setup

If your PHP task is to capture a clean screenshot of a web page rather than implement retries for a general HTTP API, ScreenshotNeo provides a website screenshot API and MCP server. Its cURL request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options. Cookie and consent banners are accepted like a visitor, then removed along with 60+ known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for the free plan.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Does this example retry HTTP 429 responses?

No. It retries transfer failures only. Whether to retry 429, and how to handle any service-provided delay, depends on the endpoint’s guidance and your application’s policy.

Can I use the same pattern with a POST request?

Not without first confirming that repeating the operation is safe. A transfer failure does not prove the server did not process the first request.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.