DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
API Development

How to Retry Failed cURL Requests in PHP Safely

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

Retry a PHP cURL request by checking whether the transfer failed, capturing the cURL error while the handle is open, and making another attempt only under a finite, time-bounded policy. Keep HTTP status handling separate: by default, curl_exec() can return a response body for a 404 because the server responded successfully at the transport level. Before retrying, make sure repeating the request is safe for that endpoint.

Separate cURL transfer failures from HTTP errors

With CURLOPT_RETURNTRANSFER enabled, curl_exec() returns the response body when the transfer succeeds and false when the transfer fails. Test with strict comparison: $body === false. Do not treat a non-2xx HTTP status as a transfer failure automatically; a 404 response can still arrive through a successful transfer. PHP’s curl_exec documentation explicitly notes that response status codes such as 404 are not regarded as failure.

That distinction determines what your retry loop should do:

  • Transfer failure: curl_exec() returned false. Read curl_errno() and curl_error() before closing the handle, then decide whether the particular failure is transient and retryable.
  • HTTP response: curl_exec() returned a body. Read the HTTP status with curl_getinfo(), then apply the endpoint’s status policy. A 404, 429, or 503 is an HTTP-level result, not inherently a cURL transfer failure.

Do not automatically retry every failure class. For example, retrying a request because it returned an HTTP response requires a status-specific policy, while a transfer error needs a cURL-error policy.

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

A bounded PHP retry example

This example retries transfer failures only. It accepts a successful 2xx response and raises an exception for other HTTP statuses without retrying them. It is intended for a GET-like request that is safe to repeat; adapt the policy to the upstream service and the application’s latency budget.

<?php
function getWithRetries(string $url, int $maxAttempts = 3): string
{
    if ($maxAttempts < 1) {
        throw new InvalidArgumentException('maxAttempts must be at least 1');
    }

    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        $ch = curl_init($url);
        if ($ch === false) {
            throw new RuntimeException('Could not initialize cURL');
        }

        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);

            if ($status >= 200 && $status < 300) {
                return $body;
            }

            throw new RuntimeException("HTTP status {$status}");
        }

        // Capture diagnostics before closing or discarding the handle.
        $errno = curl_errno($ch);
        $error = curl_error($ch);
        curl_close($ch);

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

        // Example delay: 100 ms after attempt 1, 200 ms after attempt 2.
        usleep(100_000 * $attempt);
    }

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

The defaults allow at most three attempts, with a 5-second connection timeout and a 15-second total transfer timeout for each attempt. Those are example policy values, not universal recommendations. A slow upstream or a tightly bounded web request may need different limits. Also, the loop’s per-attempt limits do not enforce one overall wall-clock deadline for all attempts and delays.

What to change for your endpoint

  • Set $maxAttempts to a finite value that fits the caller’s deadline.
  • Choose connection and total timeouts based on the upstream service and how long the application can wait.
  • Define which transfer errors, if any, merit another attempt instead of retrying every failure indiscriminately.
  • For HTTP responses, identify eligible statuses separately. Do not add HTTP retries without considering the service’s rules and, where relevant, Retry-After.
  • Use a delay strategy that fits the service’s rate limits and the application’s latency budget. The linear delay above is illustrative; it does not include jitter or server-directed timing.

Make retries safe before adding them

A retry sends another request to the server. For a read-only GET, repeating the operation is often suitable, but the endpoint’s behavior is decisive. For a request that creates, charges, deletes, or otherwise changes state, a timeout does not tell you whether the server performed the operation before the connection failed. Repeating it blindly can duplicate the effect.

Before retrying a side-effecting request, confirm that the endpoint supports a safe repeat mechanism, such as an idempotency key, and use it consistently with the same logical operation. The PHP cURL API does not prescribe a universal idempotency policy; follow the upstream API’s documented semantics. If the endpoint offers no way to make repetition safe, surface the uncertain outcome for application-level handling rather than automatically resending.

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

Timeouts and the total deadline

Use both CURLOPT_CONNECTTIMEOUT and CURLOPT_TIMEOUT. The first limits how long cURL waits to establish a connection; the second limits the total transfer time. The total timeout includes connection time, as described in libcurl’s CURLOPT_TIMEOUT documentation. Set limits that fit within the caller’s own deadline, leaving room for processing and response delivery.

Per-attempt timeouts alone do not bound the entire retry operation tightly: several attempts, plus sleeps, can exceed the time budget. If an overall deadline matters, track elapsed time or a deadline before each attempt and delay, and stop when there is not enough budget for another try. Include the time reserved for the request itself when deciding whether another attempt can begin.

HTTP status policy and CURLOPT_FAILONERROR

By default, inspect the status after a body is returned. curl_getinfo($ch, CURLINFO_RESPONSE_CODE) provides the response code while the handle is available. Then classify the result according to the endpoint: a 404 might mean the resource does not exist, while a temporary service response might be handled differently if the API documents that behavior.

CURLOPT_FAILONERROR changes cURL’s handling of HTTP response codes at or above 400. If you enable it, account for that behavior in your diagnostics and retry classification; a failure surfaced at the cURL layer no longer follows the simple separation used in the example. Explicitly checking the HTTP response code usually makes it easier to keep transport errors distinct from HTTP results. See PHP’s cURL options and constants reference.

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

Capture useful diagnostics

On a transfer failure, call curl_errno($ch) and curl_error($ch) before closing the handle. The error number supports programmatic classification; the message is for human diagnosis. When there is no error, the number is zero and the message is an empty string, according to the PHP references for curl_errno and curl_error.

Log the attempt number, endpoint identity (without secrets), error number, elapsed time, and—when a response arrived—HTTP status. Do not log authorization headers, API keys, cookies, or sensitive request bodies. Keep the original failure details available when the final attempt fails so that a retry loop does not erase the useful cause.

Retrying HTTP responses requires a separate policy

The example intentionally does not retry HTTP statuses. To do so, move status classification into an explicit branch and define which responses are eligible for that endpoint. Consider the service’s documentation for temporary errors, rate limiting, and any Retry-After header. Respect a server-provided wait instruction where applicable, while still enforcing your overall deadline and maximum attempts.

A status retry can be unsafe too: a server might have completed a state change but failed while forming or delivering the response. Apply the same repeatability test as for transport failures. Neither PHP’s cURL functions nor libcurl’s timeout documentation sets a universal retry count, retryable status list, backoff algorithm, jitter policy, or idempotency rule.

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

Multi-handle requests need per-transfer results

If you use cURL multi handles, do not carry over the single-handle error check as if it described every transfer. PHP’s curl_errno documentation directs multi-handle users to the individual result returned by curl_multi_info_read(). Associate each result with the corresponding request, then apply that request’s retry policy independently.

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

Troubleshooting common retry problems

A 404 response does not enter the retry branch

That is expected with the default behavior. A 404 is an HTTP response, so curl_exec() may return its body rather than false. Read the response code and decide whether that status should cause an application-level error or a policy-driven retry.

The error message is empty

curl_error() returns an empty string when there was no cURL error. Check the strict === false result and capture diagnostics only in that branch. For successful transfers, inspect the HTTP status instead.

The loop retries a request that should not be repeated

Check the endpoint’s side effects and idempotency support. A transfer failure cannot establish whether the server received or acted on the request. Disable automatic retries until the request can be repeated safely.

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

The operation takes longer than the expected timeout

CURLOPT_TIMEOUT applies to each transfer, not the whole loop. Account for all attempt durations and delays with an overall deadline. The connection timeout is included in the total transfer timeout, not added on top of it.

HTTP errors appear as cURL failures

Review whether CURLOPT_FAILONERROR is enabled. If it is, decide whether to keep that behavior or handle response codes explicitly with curl_getinfo() so transport diagnostics and HTTP policy remain clear.

Or skip the browser setup

If what you need is a website screenshot rather than a general-purpose PHP HTTP request, ScreenshotNeo provides a screenshot API. A GET request with a URL returns a PNG, JPEG, WebP, or PDF; its response also identifies whether the page was clean, blocked, blank, timed out, or otherwise failed, and only clean shots are billed. This is not a replacement for cURL retry logic for arbitrary APIs.

Example cURL call (see the ScreenshotNeo API documentation for request options):

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. See ScreenshotNeo for details, or sign up free.

Frequently Asked Questions

Why does curl_exec return false?

With CURLOPT_RETURNTRANSFER enabled, false signals a cURL transfer failure. Capture curl_errno() and curl_error() before closing the handle to diagnose it.

Does curl_exec fail on a 404 response?

Not by default. A 404 is an HTTP response; curl_exec can return its body. Check the response code separately.

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.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.