Friday, July 24, 2026
HomePHPPHP curl_multi: Ship A number of API Requests Concurrently

PHP curl_multi: Ship A number of API Requests Concurrently


Calling one API from PHP is straightforward. Calling three APIs one after one other can also be easy, however it may make the consumer wait far longer than needed. Every request will get its personal personal flip. Very well mannered. Not very quick.

Suppose a web page wants a buyer profile, current orders, and notifications. If these APIs take 700, 1,100, and 900 milliseconds, sequential requests want about 2.7 seconds. With PHP curl_multi, the requests can run concurrently. The overall time then stays near the slowest request, which is about 1.1 seconds on this instance.

This tutorial builds a working benchmark that compares each approaches. It additionally handles timeouts, HTTP standing codes, cURL errors, invalid JSON, and connection limits. If you happen to want a refresher on particular person requests first, see this PHP cURL information.

Fast Reply

Use curl_multi_init() to create a multi deal with. Add every request with curl_multi_add_handle(), drive the transfers with curl_multi_exec(), and wait effectively with curl_multi_select().

The requests carry out community I/O concurrently. This isn’t PHP multithreading, and it doesn’t make CPU-heavy work run in parallel.

<?php

$urls = [
    'profile' => 'https://api.example.com/profile',
    'orders' => 'https://api.example.com/orders',
    'notifications' => 'https://api.example.com/notifications'
];

$multiHandle = curl_multi_init();
$handles = [];

foreach ($urls as $identify => $url) {
    $deal with = curl_init($url);

    curl_setopt_array($deal with, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 3,
        CURLOPT_TIMEOUT => 10
    ]);

    curl_multi_add_handle($multiHandle, $deal with);
    $handles[$name] = $deal with;
}

do {
    $standing = curl_multi_exec($multiHandle, $operating);

    if ($standing !== CURLM_OK) {
        throw new RuntimeException(curl_multi_strerror($standing));
    }

    if ($operating > 0 && curl_multi_select($multiHandle, 1.0) === -1) {
        usleep(1000);
    }
} whereas ($operating > 0);

$responses = [];

foreach ($handles as $identify => $deal with) {
    $responses[$name] = curl_multi_getcontent($deal with);

    curl_multi_remove_handle($multiHandle, $deal with);
    curl_close($deal with);
}

curl_multi_close($multiHandle);

That is the essential circulate. The whole venture provides response validation, particular person error reporting, timing knowledge, and safer request settings.

What This PHP curl_multi Instance Builds

The instance creates a small dashboard that requests knowledge from three impartial API endpoints:

  • A buyer profile endpoint with a simulated delay of 700 milliseconds
  • An orders endpoint with a simulated delay of 1,100 milliseconds
  • A notifications endpoint with a simulated delay of 900 milliseconds

The primary benchmark sends these requests sequentially. PHP waits for one response earlier than beginning the subsequent request.

API request Simulated response time
Buyer profile 700 ms
Current orders 1,100 ms
Notifications 900 ms
Approximate sequential time 2,700 ms

The second benchmark begins all three requests by way of one cURL multi deal with. Whereas one endpoint is ready, the opposite transfers can proceed. The overall time is subsequently near the slowest response, as an alternative of the sum of all three responses.

Request methodology Approximate whole time
Sequential cURL requests 2.7 seconds
Concurrent curl_multi requests 1.1 seconds
Time saved About 1.6 seconds

These numbers should not hard-coded into the dashboard. PHP measures each runs with hrtime(). Small variations between runs are regular, however the concurrent model ought to stay a lot nearer to the longest particular person request.

PHP curl_multi sequential and concurrent API request benchmark

PHP curl_multi completes three concurrent API requests sooner than sequential cURL requests.

The efficiency enchancment comes from overlapping community wait time. It doesn’t make a person API reply sooner. If one endpoint takes ten seconds, the whole group can nonetheless take about ten seconds. PHP can not persuade a gradual API to drink extra espresso.

How PHP curl_multi Works

A traditional curl_exec() name blocks the PHP script till that switch finishes. When a number of calls are positioned inside a loop, the ready time grows with each request.

The cURL multi interface adjustments the circulate. It retains a number of particular person cURL handles inside one multi deal with and lets their community exercise progress collectively.

  1. Create one regular cURL deal with for every API URL.
  2. Create a multi deal with with curl_multi_init().
  3. Add each request with curl_multi_add_handle().
  4. Name curl_multi_exec() till no switch is operating.
  5. Use curl_multi_select() whereas ready for community exercise.
  6. Learn every response with curl_multi_getcontent().
  7. Take away and shut all handles.

Why curl_multi_exec() Runs Inside a Loop

A single name to curl_multi_exec() doesn’t imply each request has completed. It solely asks libcurl to carry out the work that’s presently attainable.

The $operating argument receives the variety of energetic transfers. PHP should maintain calling the perform till that worth reaches zero.

do {
    $standing = curl_multi_exec($multiHandle, $operating);
} whereas ($operating > 0);

This loop works, nevertheless it has an issue. It may possibly repeatedly name curl_multi_exec() whereas nothing is prepared, losing CPU time.

Use curl_multi_select() to Keep away from a Busy Loop

curl_multi_select() pauses the script till one of many energetic connections could make progress or the timeout expires. That is extra environment friendly than checking the transfers repeatedly.

do {
    $standing = curl_multi_exec($multiHandle, $operating);

    if ($standing !== CURLM_OK) {
        throw new RuntimeException(curl_multi_strerror($standing));
    }

    if ($operating > 0) {
        $prepared = curl_multi_select($multiHandle, 1.0);

        if ($prepared === -1) {
            usleep(1000);
        }
    }
} whereas ($operating > 0);

The quick sleep handles a lesser-known edge case. Some libcurl builds can return -1 when no file descriptor is prepared. With out the sleep, the loop could spin rapidly and eat pointless CPU.

Multi Errors and Request Errors Are Totally different

The outcome from curl_multi_exec() reviews errors affecting the whole multi stack. A CURLM_OK outcome doesn’t assure that each API request succeeded.

Every accomplished deal with should nonetheless be checked individually for:

  • Connection failures reported by curl_error()
  • Timeouts and different switch errors
  • HTTP error responses akin to 404 or 500
  • Empty or invalid JSON response our bodies

This distinction is straightforward to overlook. The multi operation could succeed completely whereas one API quietly returns an error web page sporting a JSON identify tag.

PHP curl_multi Challenge Construction

This instance makes use of plain PHP. It doesn’t want a framework, Composer package deal, JavaScript library, or database.

The venture retains the HTTP shopper separate from the web page that shows the benchmark. It additionally features a native mock API, so the timing check doesn’t rely on an exterior service.

php-curl-multi/
├── config.php
├── mock-api/
│   └── index.php
├── public/
│   ├── property/
│   │   └── type.css
│   └── index.php
├── src/
│   └── ApiClient.php
└── README.md
  • config.php comprises the API URL, timeouts, and connection restrict.
  • mock-api/index.php returns pattern JSON responses with managed delays.
  • src/ApiClient.php sends sequential and concurrent requests.
  • public/index.php runs the benchmark and shows the outcomes.
  • public/property/type.css gives the small responsive structure.

Create the Configuration File

The configuration retains values that will change between improvement and manufacturing exterior the HTTP shopper class.

<?php

declare(strict_types=1);

return [
    'api_base_url' => rtrim(
        getenv('DEMO_API_BASE_URL') ?: 'http://127.0.0.1:8001',
        '/'
    ),
    'connect_timeout_ms' => 1000,
    'request_timeout_ms' => 5000,
    'max_concurrent_requests' => 5,
];

The connection timeout controls how lengthy cURL could spend establishing a connection. The request timeout covers the whole switch.

The concurrency restrict prevents the applying from opening an extreme variety of connections directly. This demo sends solely three requests, however protecting the restrict within the configuration makes the shopper safer to reuse.

Create the Native Mock API

The mock API accepts a useful resource question parameter. It returns profile, order, or notification knowledge after a brief delay.

Create mock-api/index.php with the next code:

<?php

declare(strict_types=1);

header('Content material-Sort: software/json; charset=utf-8');
header('Cache-Management: no-store');

$useful resource = $_GET['resource'] ?? '';

$responses = [
    'profile' => [
        'delay_ms' => 700,
        'data' => [
            'name' => 'Maya Chen',
            'email' => 'maya@example.com',
            'membership' => 'Gold',
        ],
    ],
    'orders' => [
        'delay_ms' => 1100,
        'data' => [
            'count' => 3,
            'latest_order' => '#1048',
            'total' => '$184.50',
        ],
    ],
    'notifications' => [
        'delay_ms' => 900,
        'data' => [
            'unread' => 4,
            'latest' => 'Your order has been shipped.',
        ],
    ],
];

if (!isset($responses[$resource])) {
    http_response_code(404);

    echo json_encode([
        'error' => 'Unknown API resource.',
    ], JSON_THROW_ON_ERROR);

    exit;
}

$response = $responses[$resource];

usleep($response['delay_ms'] * 1000);

echo json_encode([
    'resource' => $resource,
    'simulated_delay_ms' => $response['delay_ms'],
    'knowledge' => $response['data'],
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

The delay is intentional. It makes the distinction between sequential and concurrent requests straightforward to see. In an actual venture, the ready time would normally come from a distant API, database-backed service, fee gateway, or one other server.

The API additionally returns a correct 404 response for an unknown useful resource. This provides the shopper a practical HTTP error to deal with as an alternative of assuming that each response will likely be profitable.

Create the Reusable PHP API Consumer

Create src/ApiClient.php. This class comprises each request strategies, so the benchmark can examine them underneath the identical timeout and response-handling guidelines.

<?php

declare(strict_types=1);

ultimate class ApiClient
{
    public perform __construct(
        personal readonly int $connectTimeoutMs = 1000,
        personal readonly int $requestTimeoutMs = 5000,
        personal readonly int $maxConcurrentRequests = 5
    ) {
    }

    public perform fetchSequential(array $requests): array
    {
        $startedAt = hrtime(true);
        $responses = [];

        foreach ($requests as $identify => $url) {
            $deal with = $this->createHandle($url);
            $physique = curl_exec($deal with);

            $responses[$name] = $this->buildResponse(
                $deal with,
                $physique
            );

            curl_close($deal with);
        }

        return [
            'duration_ms' => $this->elapsedMilliseconds($startedAt),
            'responses' => $responses,
        ];
    }

    public perform fetchConcurrent(array $requests): array
    {
        $startedAt = hrtime(true);
        $multiHandle = curl_multi_init();
        $handles = [];

        curl_multi_setopt(
            $multiHandle,
            CURLMOPT_MAX_TOTAL_CONNECTIONS,
            $this->maxConcurrentRequests
        );

        attempt {
            foreach ($requests as $identify => $url) {
                $deal with = $this->createHandle($url);

                $handles[$name] = [
                    'handle' => $handle,
                ];

                $standing = curl_multi_add_handle(
                    $multiHandle,
                    $deal with
                );

                if ($standing !== CURLM_OK) {
                    throw new RuntimeException(
                        curl_multi_strerror($standing)
                    );
                }
            }

            do {
                $standing = curl_multi_exec(
                    $multiHandle,
                    $operating
                );

                if ($standing !== CURLM_OK) {
                    throw new RuntimeException(
                        curl_multi_strerror($standing)
                    );
                }

                if ($operating > 0) {
                    $prepared = curl_multi_select(
                        $multiHandle,
                        1.0
                    );

                    if ($prepared === -1) {
                        usleep(1000);
                    }
                }
            } whereas ($operating > 0);

            $responses = [];

            foreach ($handles as $identify => $merchandise) {
                $deal with = $merchandise['handle'];
                $physique = curl_multi_getcontent($deal with);

                $responses[$name] = $this->buildResponse(
                    $deal with,
                    $physique
                );
            }

            return [
                'duration_ms' => $this->elapsedMilliseconds(
                    $startedAt
                ),
                'responses' => $responses,
            ];
        } lastly {
            foreach ($handles as $merchandise) {
                curl_multi_remove_handle(
                    $multiHandle,
                    $merchandise['handle']
                );

                curl_close($merchandise['handle']);
            }

            curl_multi_close($multiHandle);
        }
    }

    personal perform createHandle(string $url): CurlHandle
     CURLPROTO_HTTPS,
        ]);

        return $deal with;
    

    personal perform buildResponse(
        CurlHandle $deal with,
        string|bool $physique
    ): array {
        $curlError = curl_error($deal with);

        $statusCode = (int) curl_getinfo(
            $deal with,
            CURLINFO_RESPONSE_CODE
        );

        $durationMs = spherical(
            (float) curl_getinfo(
                $deal with,
                CURLINFO_TOTAL_TIME
            ) * 1000,
            1
        );

        if ($physique === false || $curlError !== '') {
            return [
                'ok' => false,
                'status' => $statusCode,
                'duration_ms' => $durationMs,
                'data' => null,
                'error' => $curlError !== ''
                    ? $curlError
                    : 'The request failed.',
            ];
        }

        if ($statusCode < 200 || $statusCode >= 300) {
            return [
                'ok' => false,
                'status' => $statusCode,
                'duration_ms' => $durationMs,
                'data' => null,
                'error' => 'The API returned HTTP status '
                    . $statusCode
                    . '.',
            ];
        }

        attempt {
            $knowledge = json_decode(
                $physique,
                true,
                512,
                JSON_THROW_ON_ERROR
            );
        } catch (JsonException $exception) {
            return [
                'ok' => false,
                'status' => $statusCode,
                'duration_ms' => $durationMs,
                'data' => null,
                'error' => 'Invalid JSON response: '
                    . $exception->getMessage(),
            ];
        }

        return [
            'ok' => true,
            'status' => $statusCode,
            'duration_ms' => $durationMs,
            'data' => $data,
            'error' => null,
        ];
    }

    personal perform elapsedMilliseconds(
        int $startedAt
    ): float {
        return spherical(
            (hrtime(true) - $startedAt) / 1_000_000,
            1
        );
    }
}

How the Sequential Technique Works

fetchSequential() creates and executes one deal with at a time. The following loop iteration can not start till curl_exec() returns.

That is helpful because the baseline. It reveals how a lot time the applying would spend if it made the identical API calls with out concurrency.

How the Concurrent Technique Works

fetchConcurrent() creates the identical particular person handles, however provides them to 1 multi deal with earlier than execution begins.

The associative request identify is saved with every deal with. This lets the strategy return predictable keys akin to profile, orders, and notifications, even when the responses end in a distinct order.

The lastly block removes and closes each deal with even when an exception happens. Community code has sufficient methods to misbehave with out leaving cleanup to good luck.

Validate Each API Response

The buildResponse() methodology treats transport errors, HTTP errors, and invalid JSON as separate failures. This makes error messages extra helpful throughout debugging.

Profitable HTTP transport doesn’t assure legitimate JSON. The instance makes use of JSON_THROW_ON_ERROR so malformed responses can not silently develop into null. For extra examples, see the PHPpot information to PHP JSON encode and decode.

Every outcome follows the identical construction:

[
    'ok' => true,
    'status' => 200,
    'duration_ms' => 703.4,
    'data' => [
        // Decoded API response
    ],
    'error' => null,
]

A constant response format retains the show code easy. It additionally prevents profitable knowledge and error messages from changing into an thrilling assortment of particular instances.

Construct the Benchmark Web page

Create public/index.php. This web page defines the three API requests, runs each shopper strategies, and shows the measured time.

<?php

declare(strict_types=1);

require_once dirname(__DIR__) . '/src/ApiClient.php';

$config = require dirname(__DIR__) . '/config.php';

$error = null;
$sequential = null;
$concurrent = null;

if (!extension_loaded('curl')) {
    $error="The PHP cURL extension isn't enabled.";
} else {
    $requests = [
        'profile' => $config['api_base_url']
            . '/?useful resource=profile',
        'orders' => $config['api_base_url']
            . '/?useful resource=orders',
        'notifications' => $config['api_base_url']
            . '/?useful resource=notifications',
    ];

    attempt {
        $shopper = new ApiClient(
            $config['connect_timeout_ms'],
            $config['request_timeout_ms'],
            $config['max_concurrent_requests']
        );

        $sequential = $client->fetchSequential($requests);
        $concurrent = $client->fetchConcurrent($requests);
    } catch (Throwable $exception) {
        $error = $exception->getMessage();
    }
}

perform escape(blended $worth): string
 ENT_SUBSTITUTE,
        'UTF-8'
    );


perform seconds(float $milliseconds): string
{
    return number_format(
        $milliseconds / 1000,
        2
    ) . ' seconds';
}

$timeSaved = $sequential && $concurrent
    ? max(
        0,
        $sequential['duration_ms']
            - $concurrent['duration_ms']
    )
    : 0;
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta
        identify="viewport"
        content material="width=device-width, initial-scale=1.0"
    >
    <title>PHP curl_multi API Benchmark</title>
    <hyperlink rel="stylesheet" href="https://phppot.com/php/php-curl-multi/property/type.css">
</head>
<physique>
<foremost class="page-shell">
    <header class="page-header">
        <p class="eyebrow">PHP cURL benchmark</p>

        <h1>Sequential vs concurrent API requests</h1>

        <p class="intro">
            The identical three API endpoints are referred to as twice.
            The primary run waits for every response.
            The second run makes use of
            <code>curl_multi</code>.
        </p>
    </header>

    <?php if ($error !== null): ?>
        <div class="message message-error">
            <robust>Unable to run the benchmark.</robust>
            <span><?= escape($error) ?></span>
        </div>
    <?php else: ?>
        <part
            class="benchmark-grid"
            aria-label="Benchmark outcomes"
        >
            <article class="metric-card">
                <span class="metric-label">
                    Sequential requests
                </span>

                <robust>
                    <?= escape(
                        seconds($sequential['duration_ms'])
                    ) ?>
                </robust>

                <small>
                    One request finishes earlier than the subsequent begins.
                </small>
            </article>

            <article
                class="metric-card metric-card-highlight"
            >
                <span class="metric-label">
                    Concurrent requests
                </span>

                <robust>
                    <?= escape(
                        seconds($concurrent['duration_ms'])
                    ) ?>
                </robust>

                <small>
                    All requests look forward to community responses
                    collectively.
                </small>
            </article>

            <article class="metric-card">
                <span class="metric-label">
                    Time saved
                </span>

                <robust>
                    <?= escape(seconds($timeSaved)) ?>
                </robust>

                <small>
                    Your outcome will range barely between runs.
                </small>
            </article>
        </part>

        <part class="results-section">
            <div class="section-heading">
                <div>
                    <p class="eyebrow">
                        Concurrent response particulars
                    </p>

                    <h2>One outcome for every API</h2>
                </div>

                <a category="button" href="index.php">
                    Run benchmark once more
                </a>
            </div>

            <div class="response-grid">
                <?php foreach (
                    $concurrent['responses']
                    as $identify => $response
                ): ?>
                    <article class="response-card">
                        <div class="response-title">
                            <h3>
                                <?= escape(ucfirst($identify)) ?>
                            </h3>

                            <span class="standing <?=
                                $response['ok']
                                    ? 'status-ok'
                                    : 'status-error'
                            ?>">
                                HTTP
                                <?= escape($response['status']) ?>
                            </span>
                        </div>

                        <p class="response-time">
                            Accomplished in
                            <?= escape(
                                $response['duration_ms']
                            ) ?>
                            ms
                        </p>

                        <?php if ($response['ok']): ?>
                            <pre><?= escape(
                                json_encode(
                                    $response['data']['data'],
                                    JSON_PRETTY_PRINT
                                        | JSON_UNESCAPED_SLASHES
                                )
                            ) ?></pre>
                        <?php else: ?>
                            <div
                                class="message message-error"
                            >
                                <?= escape(
                                    $response['error']
                                ) ?>
                            </div>
                        <?php endif; ?>
                    </article>
                <?php endforeach; ?>
            </div>
        </part>
    <?php endif; ?>
</foremost>
</physique>
</html>

Run the Identical Request Checklist Twice

Each strategies obtain the identical associative array of API URLs. This retains the comparability honest and makes it straightforward to attach every response to its objective.

The overall length is measured contained in the shopper. The web page subtracts the concurrent length from the sequential length to calculate the time saved.

Escape API Knowledge Earlier than Displaying It

API responses are exterior enter. Even a trusted service can return surprising content material after a configuration mistake or safety incident.

The escape() perform applies htmlspecialchars() earlier than values are printed into the web page. JSON proven contained in the response playing cards is escaped as properly. Receiving JSON doesn’t make its values routinely protected for HTML output.

The web page additionally checks whether or not the cURL extension is out there. Whether it is lacking, the consumer sees a helpful message as an alternative of PHP introducing the issue with a deadly error.

Run the PHP curl_multi Challenge Domestically

Be certain that PHP 8.1 or later is put in and the cURL extension is enabled.

You’ll be able to verify the extension from the command line:

php -m | grep curl

If cURL is enabled, the command prints curl.

Begin the Mock API

Open a terminal within the venture listing. On macOS or Linux, begin the mock API with a number of PHP development-server staff:

PHP_CLI_SERVER_WORKERS=4 php -S 127.0.0.1:8001 -t mock-api

The mock API is now out there at URLs akin to:

http://127.0.0.1:8001/?useful resource=profile
http://127.0.0.1:8001/?useful resource=orders
http://127.0.0.1:8001/?useful resource=notifications

Begin the Demo Web page

Open a second terminal in the identical venture listing:

php -S 127.0.0.1:8000 -t public

Then open the next URL in a browser:

http://127.0.0.1:8000

The web page runs each benchmarks routinely. With the equipped delays, the sequential check ought to take about 2.7 seconds. The concurrent check ought to end in about 1.1 seconds.

Vital PHP Improvement Server Caveat

PHP’s built-in improvement server makes use of one employee by default. A single employee can course of just one request at a time.

If the benchmark web page and mock endpoints are positioned on the identical single-worker server, the requests could develop into sequential. They could even wait eternally as a result of the present PHP request is attempting to name one other URL dealt with by the employee it already occupies.

That is why the instance makes use of two ports and begins the mock API with a number of staff. It’s not merely ornamental terminal exercise.

Run the Challenge with XAMPP, MAMP, or Apache

You can too place the venture inside your native internet root. Apache usually has a number of staff out there, so it may serve the mock API requests concurrently.

For instance, the URLs could also be:

Demo web page:
http://localhost/php-curl-multi/public/

Mock API:
http://localhost/php-curl-multi/mock-api/

Replace api_base_url in config.php:

'api_base_url' => 'http://localhost/php-curl-multi/mock-api',

You can too set the API URL by way of an surroundings variable:

export DEMO_API_BASE_URL="http://localhost/php-curl-multi/mock-api"

If the concurrent result’s virtually as gradual because the sequential outcome, test the mock API server first. The commonest trigger is an area server that also processes the API requests one after the other.

When Concurrent API Requests Enhance Efficiency

curl_multi works finest when a PHP web page wants a number of impartial HTTP responses earlier than it may proceed.

Good examples embody:

  • Loading profile, order, and notification knowledge from separate providers
  • Accumulating costs from a number of provider APIs
  • Checking the standing of a number of distant servers
  • Fetching reviews from impartial endpoints
  • Sending the identical webhook payload to a number of locations

The vital phrase is impartial. If one request wants knowledge returned by one other request, these two calls can not begin collectively.

Unbiased Requests Can Run Concurrently

$requests = [
    'profile' => $profileUrl,
    'orders' => $ordersUrl,
    'notifications' => $notificationsUrl,
];

$outcomes = $client->fetchConcurrent($requests);

None of those URLs is dependent upon one other response. They’re good candidates for curl_multi.

Dependent Requests Should Hold Their Order

$loginResponse = sendRequest($loginUrl);

$accessToken = $loginResponse['access_token'];

$profileResponse = sendAuthenticatedRequest(
    $profileUrl,
    $accessToken
);

The profile request wants the entry token returned by the login request. Beginning each calls collectively would solely assist them fail sooner.

When curl_multi Will Not Assist

Scenario Why curl_multi doesn’t remedy it
One gradual API request There isn’t any different community wait time to overlap.
CPU-heavy PHP calculations curl_multi manages community transfers, not PHP computation.
Requests that rely on earlier responses Later calls can not begin till the required knowledge exists.
An API with strict fee limits Extra simultaneous requests could set off throttling.
A server that handles one request at a time The distant facet nonetheless processes the calls sequentially.

The Slowest Request Nonetheless Units the Tempo

Concurrent requests cut back the full from roughly the sum of all response occasions to roughly the longest response time.

For 3 requests taking one, two, and 5 seconds:

  • Sequential time is roughly eight seconds.
  • Concurrent time is roughly 5 seconds.

The five-second API stays a five-second API. curl_multi merely stops the opposite requests from ready in line behind it.

Do Not Ship Lots of of Requests at As soon as

Concurrency wants a restrict. Opening too many connections can enhance reminiscence use, overload the distant service, or trigger HTTP 429 responses.

The instance limits the whole multi deal with with:

curl_multi_setopt(
    $multiHandle,
    CURLMOPT_MAX_TOTAL_CONNECTIONS,
    5
);

Select the restrict based mostly on the API documentation, server capability, and fee coverage. 5 or ten concurrent requests could also be affordable for one service. 5 hundred is normally a inventive method to meet its safety workforce.

Frequent PHP curl_multi Errors and Fixes

curl_multi_exec() Returns CURLM_OK, however a Request Failed

CURLM_OK means the multi stack operated appropriately. It doesn’t imply each particular person switch succeeded.

Test every deal with after execution:

$curlError = curl_error($deal with);

$statusCode = (int) curl_getinfo(
    $deal with,
    CURLINFO_RESPONSE_CODE
);

if ($curlError !== '') {
    echo 'cURL error: ' . $curlError;
}

if ($statusCode < 200 || $statusCode >= 300) {
    echo 'HTTP error: ' . $statusCode;
}

A request can outing, fail DNS lookup, or return HTTP 500 whereas the multi deal with itself stays completely content material.

curl_multi_select() Returns -1

Some libcurl builds could return -1 when no file descriptor is prepared. Add a brief sleep earlier than persevering with the loop:

$prepared = curl_multi_select($multiHandle, 1.0);

if ($prepared === -1) {
    usleep(1000);
}

This prevents the loop from repeatedly checking the connections and consuming pointless CPU.

The Concurrent Model Is Not Sooner

Test whether or not the requests are actually impartial and whether or not the API server can course of a couple of request at a time.

Throughout native testing, a single-worker PHP improvement server is a typical trigger. Use a number of API staff or run the mock endpoints by way of Apache or Nginx.

Concurrency might also present little enchancment when:

  • The API responses are already extraordinarily quick.
  • More often than not is spent processing knowledge after obtain.
  • The distant server queues requests from the identical shopper.
  • The API applies a low concurrency restrict.

A Request By no means Finishes

Each deal with ought to have each a connection timeout and a complete timeout:

curl_setopt_array($deal with, [
    CURLOPT_CONNECTTIMEOUT_MS => 1000,
    CURLOPT_TIMEOUT_MS => 5000,
]);

The connection timeout covers DNS lookup and connection setup. The overall timeout limits the whole request.

With out these values, one unresponsive endpoint can maintain your complete group ready. Concurrent doesn’t imply immortal.

The API Returns HTTP 429

HTTP 429 means the service is rate-limiting the shopper. Scale back the concurrency restrict and examine the response headers for retry data.

A manufacturing shopper could must:

  • Respect the API’s documented request restrict.
  • Look forward to the length given by a Retry-After header.
  • Retry solely protected requests.
  • Use exponential backoff as an alternative of retrying instantly.

Don’t deal with retries as permission to hammer the identical endpoint with larger willpower.

json_decode() Returns null

A response could include invalid JSON, an empty physique, or an HTML error web page. Use JSON_THROW_ON_ERROR to make the failure seen:

attempt {
    $knowledge = json_decode(
        $physique,
        true,
        512,
        JSON_THROW_ON_ERROR
    );
} catch (JsonException $exception) {
    $error="Invalid JSON response: "
        . $exception->getMessage();
}

Test the HTTP standing earlier than decoding. A server returning an HTML 500 web page isn’t a JSON parsing thriller. It’s an HTTP error sporting the mistaken outfit.

Responses Are Matched to the Mistaken Request

Don’t rely on completion order. A quick endpoint could end earlier than a request that was added earlier.

Retailer every deal with with a secure identify:

foreach ($requests as $identify => $url) {
    $deal with = curl_init($url);

    $handles[$name] = $deal with;

    curl_multi_add_handle(
        $multiHandle,
        $deal with
    );
}

When studying the outcomes, use the identical identify because the response key. This retains profile knowledge hooked up to profile, even when notifications end first.

Safety Issues

Concurrent requests don’t introduce a very new safety mannequin, however they’ll multiply the impact of a foul URL, lacking timeout, or leaked credential. Apply the identical controls to each deal with.

Do Not Settle for Arbitrary Request URLs

The instance builds its URLs from a trusted configuration worth. Don’t move a URL from $_GET, $_POST, or one other untrusted supply on to curl_init().

// Unsafe
$url = $_GET['url'] ?? '';

$deal with = curl_init($url);

This may create a server-side request forgery vulnerability. An attacker could attempt to entry inner providers, cloud metadata endpoints, or personal community addresses by way of your server.

For a manufacturing integration, maintain API endpoints in configuration or prohibit requests to an express checklist of trusted hosts.

perform isAllowedApiUrl(
    string $url,
    array $allowedHosts
): bool {
    $elements = parse_url($url);

    if (
        !isset($elements['scheme'], $elements['host'])
        || $elements['scheme'] !== 'https'
    ) {
        return false;
    }

    return in_array(
        strtolower($elements['host']),
        $allowedHosts,
        true
    );
}

$allowedHosts = [
    'api.example.com',
    'payments.example.com',
];

if (!isAllowedApiUrl($url, $allowedHosts)) {
    throw new InvalidArgumentException(
        'The API URL isn't allowed.'
    );
}

A hostname allowlist is just one layer. Functions that fetch user-influenced URLs must also shield in opposition to hostnames resolving to non-public IP addresses and DNS rebinding.

Prohibit Supported Protocols

Restrict cURL to the protocols the applying really wants:

CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,

This prevents an surprising URL from switching to protocols akin to FTP or native file entry.

Deal with Redirects Fastidiously

The instance disables automated redirects:

CURLOPT_FOLLOWLOCATION => false,

A trusted public URL can redirect to an untrusted or personal handle. If redirects are required, validate each vacation spot and set a small redirect restrict.

CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,

Protocol restriction alone doesn’t cease redirects to non-public HTTP addresses. The vacation spot nonetheless wants validation.

Hold TLS Verification Enabled

For HTTPS APIs, PHP cURL verifies the certificates and hostname by default. Don’t disable these checks to silence a certificates error.

// Don't use these settings in manufacturing.
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => 0,

Repair the server certificates or native certificates retailer as an alternative. Disabling verification permits a community attacker to intercept API credentials and responses.

Hold API Credentials Exterior the Supply Code

Learn tokens from surroundings variables or a secret supervisor:

$apiToken = getenv('API_TOKEN');

if (!$apiToken) {
    throw new RuntimeException(
        'The API token isn't configured.'
    );
}

curl_setopt($deal with, CURLOPT_HTTPHEADER, [
    'Accept: application/json',
    'Authorization: Bearer ' . $apiToken,
]);

Don’t print authorization headers in browser errors or write full tokens into software logs.

Restrict Time, Connections, and Response Dimension

Timeouts and connection limits shield software sources when an API turns into gradual or unavailable. For APIs that will return massive recordsdata or untrusted content material, additionally implement a most response measurement.

With out limits, 5 concurrent requests can develop into 5 simultaneous methods to exhaust reminiscence.

Escape Response Knowledge

JSON values should not protected HTML merely as a result of they arrived from an API. Escape values with htmlspecialchars() earlier than inserting them on a web page.

This issues even for a trusted service. Accounts could be compromised, saved knowledge can include markup, and APIs often return surprises that weren’t included within the integration assembly.

Developer FAQ

Is PHP curl_multi actually parallel?

curl_multi performs concurrent community transfers. A number of requests could make progress throughout the identical interval, however your PHP code isn’t executing in a number of CPU threads.

For HTTP requests, concurrent is the extra correct time period. Builders usually seek for “parallel cURL requests,” so each descriptions are generally used.

Does curl_multi_exec() run within the background?

No. The PHP script nonetheless waits till the multi loop finishes. The distinction is that it waits for a number of energetic transfers collectively as an alternative of finishing them one after the other.

If work should proceed after the net request ends, use a queue, employee, scheduled job, or one other background-processing system.

Can curl_multi ship POST requests?

Sure. Every deal with can have its personal HTTP methodology, headers, and request physique.

$deal with = curl_init($url);

curl_setopt_array($deal with, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode(
        $payload,
        JSON_THROW_ON_ERROR
    ),
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Accept: application/json',
    ],
    CURLOPT_RETURNTRANSFER => true,
]);

The configured deal with can then be added with curl_multi_add_handle(). See the PHP cURL POST instance for extra particulars about sending kind knowledge and JSON request our bodies.

Can GET and POST requests be blended in a single multi deal with?

Sure. Each straightforward deal with retains its personal choices. One deal with can ship GET, one other can ship POST, and one other can embody authentication headers.

The multi deal with manages when their community transfers progress. It doesn’t require each request to make use of the identical methodology or vacation spot.

Are responses returned in the identical order because the URLs?

Requests could end in any order. Don’t assume that the primary accomplished response belongs to the primary URL.

Retailer every deal with with a secure associative key and return outcomes utilizing that key. The instance makes use of profile, orders, and notifications.

What number of concurrent requests ought to PHP ship?

There isn’t any common protected quantity. It is dependent upon the distant API, response measurement, out there reminiscence, connection limits, and fee coverage.

Begin with a small worth akin to 5. Measure the outcome and respect any limits documented by the API supplier. Extra concurrency isn’t routinely extra efficiency.

Why use curl_multi_select() as an alternative of usleep() in each loop?

curl_multi_select() waits for precise community exercise. A set sleep could pause longer than needed or wake repeatedly when no switch could make progress.

A brief usleep() is used solely when curl_multi_select() returns -1.

Does curl_multi make a gradual API sooner?

No. It reduces pointless ready between impartial requests. The whole group nonetheless is dependent upon its slowest request.

If one API is persistently gradual, enhance that service, add applicable caching, request much less knowledge, or transfer nonessential work to a background course of.

Can curl_multi be used for file downloads?

Sure. It may possibly obtain a number of recordsdata concurrently. For giant recordsdata, write every response on to a file as an alternative of protecting each physique in reminiscence.

Additionally restrict concurrency and most file measurement. A small JSON response and a two-gigabyte archive have very completely different opinions about out there reminiscence.

Remaining Takeaway

PHP curl_multi is beneficial when one operation wants responses from a number of impartial APIs. It begins the transfers collectively, waits effectively for community exercise, and reduces whole ready time to roughly the length of the slowest request.

The vital elements should not restricted to calling curl_multi_exec(). A dependable implementation must also:

  • Use curl_multi_select() to keep away from a busy loop.
  • Set connection and total-request timeouts.
  • Test cURL errors for each deal with.
  • Validate HTTP standing codes individually.
  • Deal with invalid JSON responses.
  • Hold responses related to secure request names.
  • Restrict the variety of concurrent connections.
  • Prohibit URLs, protocols, and redirects.

Concurrency isn’t useful for each downside. It is not going to speed up CPU-heavy PHP code, take away API fee limits, or make a single gradual endpoint reply sooner. However when a number of impartial HTTP calls are holding up a web page, it may take away a shocking quantity of avoidable ready.

Obtain the PHP curl_multi Challenge

The downloadable ZIP comprises the whole working venture, together with the reusable API shopper, native mock API, benchmark web page, stylesheet, configuration, and setup directions.

Obtain the PHP curl_multi concurrent API requests venture

Extract the ZIP, observe the directions in README.md, and run the mock API and demo web page on separate native ports.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments