Transient API failures can make a PHP cURL client fail even though the same request would work seconds later. Add retries only around errors that can recover, such as connection failures, timeouts, rate limiting, and temporary server responses.

curl_exec() returns boolean false for cURL-level transfer failures, while HTTP responses such as 503 still need to be classified with curl_getinfo(). A retry loop should check both surfaces before deciding whether to sleep and send another attempt.

Keep the retry count small and use backoff so the client does not hammer a struggling service. Do not retry non-idempotent writes such as payments, order creation, or email sends unless the API supports an idempotency key or another duplicate-prevention mechanism.

Steps to retry a PHP cURL request:

  1. Confirm that the PHP cURL extension is loaded in the runtime that will run the request.
    $ php -r 'var_export(extension_loaded("curl")); echo PHP_EOL;'
    true

    If this prints false, install or enable the PHP cURL extension for the PHP runtime that runs the script before testing retry behavior.

  2. Create a local endpoint that returns two temporary 503 responses before succeeding.
    retry-server.php
    <?php
    $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
    $counterFile = __DIR__ . '/retry-counter.txt';
     
    header('Content-Type: application/json');
     
    if ($path !== '/orders/42') {
        http_response_code(404);
        echo json_encode(['error' => 'not found']) . PHP_EOL;
        return;
    }
     
    $attempt = is_file($counterFile) ? (int) file_get_contents($counterFile) : 0;
    $attempt++;
    file_put_contents($counterFile, (string) $attempt);
     
    if ($attempt < 3) {
        http_response_code(503);
        echo json_encode([
            'status' => 'busy',
            'attempt' => $attempt,
        ], JSON_UNESCAPED_SLASHES) . PHP_EOL;
        return;
    }
     
    echo json_encode([
        'status' => 'ok',
        'attempt' => $attempt,
    ], JSON_UNESCAPED_SLASHES) . PHP_EOL;

    The counter file makes the first two requests fail in a repeatable way. Remove retry-counter.txt before rerunning the example when you need the same failure sequence again.

  3. Start the local endpoint in a second terminal while testing the retry client.
    $ php -S 127.0.0.1:8080 retry-server.php

    Stop the built-in server with Ctrl+C after verification.

  4. Create the PHP cURL retry client script.
    retry-request.php
    <?php
    $url = getenv('API_URL') ?: 'http://127.0.0.1:8080/orders/42';
    $maxAttempts = 3;
    $baseDelaySeconds = 0.2;
    $retryStatuses = [408, 429, 500, 502, 503, 504];
    $retryCurlErrors = [
        CURLE_COULDNT_RESOLVE_HOST,
        CURLE_COULDNT_CONNECT,
        CURLE_OPERATION_TIMEDOUT,
        CURLE_RECV_ERROR,
        CURLE_SEND_ERROR,
        CURLE_GOT_NOTHING,
    ];
     
    $lastError = null;
     
    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        $curl = curl_init($url);
        if ($curl === false) {
            fwrite(STDERR, "Could not initialize cURL." . PHP_EOL);
            exit(1);
        }
     
        curl_setopt_array($curl, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                'Accept: application/json',
            ],
            CURLOPT_CONNECTTIMEOUT => 3,
            CURLOPT_TIMEOUT => 10,
        ]);
     
        $body = curl_exec($curl);
        $errno = curl_errno($curl);
        $error = curl_error($curl);
        $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);
     
        if ($body === false) {
            $lastError = "cURL error {$errno}: {$error}";
            echo "Attempt {$attempt}: {$lastError}" . PHP_EOL;
            $shouldRetry = in_array($errno, $retryCurlErrors, true);
        } else {
            echo "Attempt {$attempt}: HTTP {$status}" . PHP_EOL;
     
            if ($status >= 200 && $status < 300) {
                echo "Response body: " . trim($body) . PHP_EOL;
                exit(0);
            }
     
            $lastError = "HTTP {$status}: " . trim($body);
            $shouldRetry = in_array($status, $retryStatuses, true);
        }
     
        if (!$shouldRetry || $attempt === $maxAttempts) {
            break;
        }
     
        $delay = $baseDelaySeconds * (2 ** ($attempt - 1));
        echo "Retrying after {$delay} seconds." . PHP_EOL;
        usleep((int) ($delay * 1000000));
    }
     
    echo "Request failed after {$maxAttempts} attempts." . PHP_EOL;
    echo "Last error: {$lastError}" . PHP_EOL;
    exit(1);

    Read curl_errno() and curl_error() before closing the handle. After a completed HTTP response, use curl_getinfo() with CURLINFO_HTTP_CODE so retry decisions are based on the server status rather than the response body alone.

  5. Run the client and confirm that the third attempt succeeds after two retryable HTTP responses.
    $ php retry-request.php
    Attempt 1: HTTP 503
    Retrying after 0.2 seconds.
    Attempt 2: HTTP 503
    Retrying after 0.4 seconds.
    Attempt 3: HTTP 200
    Response body: {"status":"ok","attempt":3}

    The output proves that the client retried only the temporary 503 responses and stopped as soon as the server returned a 2xx response.

  6. Point the same client at an unused local port and confirm that retryable transport errors stop after the attempt limit.
    $ API_URL=http://127.0.0.1:8099/orders/42 php retry-request.php
    Attempt 1: cURL error 7: Failed to connect to 127.0.0.1 port 8099 after 0 ms: Could not connect to server
    Retrying after 0.2 seconds.
    Attempt 2: cURL error 7: Failed to connect to 127.0.0.1 port 8099 after 0 ms: Could not connect to server
    Retrying after 0.4 seconds.
    Attempt 3: cURL error 7: Failed to connect to 127.0.0.1 port 8099 after 0 ms: Could not connect to server
    Request failed after 3 attempts.
    Last error: cURL error 7: Failed to connect to 127.0.0.1 port 8099 after 0 ms: Could not connect to server

    The failure path proves that connection errors do not loop forever. Related: How to handle HTTP errors with PHP cURL
    Related: How to debug PHP cURL with verbose output

  7. Restrict the retry branch to temporary failures before moving the pattern into application code.

    Do not retry permanent statuses such as 400, 401, 403, or 404. Do not retry non-idempotent write requests unless the target API documents a safe duplicate-prevention mechanism, such as an idempotency key.