{"id":137770,"date":"2026-07-14T18:44:23","date_gmt":"2026-07-14T18:44:23","guid":{"rendered":"https:\/\/phppot.com\/?p=25171"},"modified":"2026-07-14T18:44:23","modified_gmt":"2026-07-14T18:44:23","slug":"php-curl_multi-send-multiple-api-requests-concurrently","status":"publish","type":"post","link":"https:\/\/sickgaming.net\/blog\/2026\/07\/14\/php-curl_multi-send-multiple-api-requests-concurrently\/","title":{"rendered":"PHP curl_multi: Send Multiple API Requests Concurrently"},"content":{"rendered":"<p>Calling one API from PHP is simple. Calling three APIs one after another is also simple, but it can make the user wait far longer than necessary. Each request gets its own private turn. Very polite. Not very fast.<\/p>\n<p>Suppose a page needs a customer profile, recent orders, and notifications. If those APIs take 700, 1,100, and 900 milliseconds, sequential requests need about 2.7 seconds. With PHP <code>curl_multi<\/code>, the requests can run concurrently. The total time then stays close to the slowest request, which is about 1.1 seconds in this example.<\/p>\n<p>This tutorial builds a working benchmark that compares both approaches. It also handles timeouts, HTTP status codes, cURL errors, invalid JSON, and connection limits. If you need a refresher on individual requests first, see this <a href=\"https:\/\/phppot.com\/php\/php-curl\/\">PHP cURL guide<\/a>.<\/p>\n<h2>Quick Answer<\/h2>\n<p>Use <a href=\"https:\/\/www.php.net\/manual\/en\/function.curl-multi-init.php\" rel=\"nofollow\">curl_multi_init()<\/a> to create a multi handle. Add each request with <code>curl_multi_add_handle()<\/code>, drive the transfers with <code>curl_multi_exec()<\/code>, and wait efficiently with <code>curl_multi_select()<\/code>.<\/p>\n<p>The requests perform network I\/O concurrently. This is not PHP multithreading, and it does not make CPU-heavy work run in parallel.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">&lt;?php $urls = [ 'profile' =&gt; 'https:\/\/api.example.com\/profile', 'orders' =&gt; 'https:\/\/api.example.com\/orders', 'notifications' =&gt; 'https:\/\/api.example.com\/notifications'\n]; $multiHandle = curl_multi_init();\n$handles = []; foreach ($urls as $name =&gt; $url) { $handle = curl_init($url); curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER =&gt; true, CURLOPT_CONNECTTIMEOUT =&gt; 3, CURLOPT_TIMEOUT =&gt; 10 ]); curl_multi_add_handle($multiHandle, $handle); $handles[$name] = $handle;\n} do { $status = curl_multi_exec($multiHandle, $running); if ($status !== CURLM_OK) { throw new RuntimeException(curl_multi_strerror($status)); } if ($running &gt; 0 &amp;&amp; curl_multi_select($multiHandle, 1.0) === -1) { usleep(1000); }\n} while ($running &gt; 0); $responses = []; foreach ($handles as $name =&gt; $handle) { $responses[$name] = curl_multi_getcontent($handle); curl_multi_remove_handle($multiHandle, $handle); curl_close($handle);\n} curl_multi_close($multiHandle);<\/code><\/pre>\n<p>This is the basic flow. The complete project adds response validation, individual error reporting, timing data, and safer request settings.<\/p>\n<h2>What This PHP curl_multi Example Builds<\/h2>\n<p>The example creates a small dashboard that requests data from three independent API endpoints:<\/p>\n<ul>\n<li>A customer profile endpoint with a simulated delay of 700 milliseconds<\/li>\n<li>An orders endpoint with a simulated delay of 1,100 milliseconds<\/li>\n<li>A notifications endpoint with a simulated delay of 900 milliseconds<\/li>\n<\/ul>\n<p>The first benchmark sends these requests sequentially. PHP waits for one response before starting the next request.<\/p>\n<table>\n<thead>\n<tr>\n<th>API request<\/th>\n<th>Simulated response time<\/th>\n<\/tr>\n<\/thead>\n<tbody readability=\"1\">\n<tr>\n<td>Customer profile<\/td>\n<td>700 ms<\/td>\n<\/tr>\n<tr>\n<td>Recent orders<\/td>\n<td>1,100 ms<\/td>\n<\/tr>\n<tr>\n<td>Notifications<\/td>\n<td>900 ms<\/td>\n<\/tr>\n<tr readability=\"2\">\n<td><strong>Approximate sequential time<\/strong><\/td>\n<td><strong>2,700 ms<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The second benchmark starts all three requests through one cURL multi handle. While one endpoint is waiting, the other transfers can continue. The total time is therefore close to the slowest response, instead of the sum of all three responses.<\/p>\n<table>\n<thead>\n<tr>\n<th>Request method<\/th>\n<th>Approximate total time<\/th>\n<\/tr>\n<\/thead>\n<tbody readability=\"1\">\n<tr>\n<td>Sequential cURL requests<\/td>\n<td>2.7 seconds<\/td>\n<\/tr>\n<tr readability=\"2\">\n<td>Concurrent curl_multi requests<\/td>\n<td>1.1 seconds<\/td>\n<\/tr>\n<tr>\n<td><strong>Time saved<\/strong><\/td>\n<td><strong>About 1.6 seconds<\/strong><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>These numbers are not hard-coded into the dashboard. PHP measures both runs with <code>hrtime()<\/code>. Small differences between runs are normal, but the concurrent version should remain much closer to the longest individual request.<\/p>\n<div id=\"attachment_25174\" class=\"wp-caption alignnone\" readability=\"32\"><img fetchpriority=\"high\" decoding=\"async\" aria-describedby=\"caption-attachment-25174\" src=\"https:\/\/phppot.com\/wp-content\/uploads\/2026\/07\/php-curl-multi-benchmark-550x362.jpg\" alt=\"PHP curl_multi sequential and concurrent API request benchmark\" width=\"550\" height=\"362\" class=\"size-large wp-image-25174\" srcset=\"https:\/\/phppot.com\/wp-content\/uploads\/2026\/07\/php-curl-multi-benchmark-550x362.jpg 550w, https:\/\/phppot.com\/wp-content\/uploads\/2026\/07\/php-curl-multi-benchmark-300x198.jpg 300w, https:\/\/phppot.com\/wp-content\/uploads\/2026\/07\/php-curl-multi-benchmark-768x506.jpg 768w, https:\/\/phppot.com\/wp-content\/uploads\/2026\/07\/php-curl-multi-benchmark.jpg 1200w\" sizes=\"(max-width: 550px) 100vw, 550px\"><\/p>\n<p id=\"caption-attachment-25174\" class=\"wp-caption-text\">PHP curl_multi completes three concurrent API requests faster than sequential cURL requests.<\/p>\n<\/div>\n<p>The performance improvement comes from overlapping network wait time. It does not make an individual API respond faster. If one endpoint takes ten seconds, the complete group can still take about ten seconds. PHP cannot persuade a slow API to drink more coffee.<\/p>\n<h2>How PHP curl_multi Works<\/h2>\n<p>A normal <code>curl_exec()<\/code> call blocks the PHP script until that transfer finishes. When several calls are placed inside a loop, the waiting time grows with every request.<\/p>\n<p>The cURL multi interface changes the flow. It keeps several individual cURL handles inside one multi handle and lets their network activity progress together.<\/p>\n<ol>\n<li>Create one normal cURL handle for each API URL.<\/li>\n<li>Create a multi handle with <code>curl_multi_init()<\/code>.<\/li>\n<li>Add every request with <code>curl_multi_add_handle()<\/code>.<\/li>\n<li>Call <code>curl_multi_exec()<\/code> until no transfer is running.<\/li>\n<li>Use <code>curl_multi_select()<\/code> while waiting for network activity.<\/li>\n<li>Read each response with <code>curl_multi_getcontent()<\/code>.<\/li>\n<li>Remove and close all handles.<\/li>\n<\/ol>\n<h3>Why curl_multi_exec() Runs Inside a Loop<\/h3>\n<p>A single call to <code>curl_multi_exec()<\/code> does not mean every request has finished. It only asks libcurl to perform the work that is currently possible.<\/p>\n<p>The <code>$running<\/code> argument receives the number of active transfers. PHP must keep calling the function until that value reaches zero.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">do { $status = curl_multi_exec($multiHandle, $running);\n} while ($running &gt; 0);<\/code><\/pre>\n<p>This loop works, but it has a problem. It can repeatedly call <code>curl_multi_exec()<\/code> while nothing is ready, wasting CPU time.<\/p>\n<h3>Use curl_multi_select() to Avoid a Busy Loop<\/h3>\n<p><code>curl_multi_select()<\/code> pauses the script until one of the active connections can make progress or the timeout expires. This is more efficient than checking the transfers continuously.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">do { $status = curl_multi_exec($multiHandle, $running); if ($status !== CURLM_OK) { throw new RuntimeException(curl_multi_strerror($status)); } if ($running &gt; 0) { $ready = curl_multi_select($multiHandle, 1.0); if ($ready === -1) { usleep(1000); } }\n} while ($running &gt; 0);<\/code><\/pre>\n<p>The short sleep handles a lesser-known edge case. Some libcurl builds can return <code>-1<\/code> when no file descriptor is ready. Without the sleep, the loop may spin quickly and consume unnecessary CPU.<\/p>\n<h3>Multi Errors and Request Errors Are Different<\/h3>\n<p>The result from <a href=\"https:\/\/www.php.net\/manual\/en\/function.curl-multi-exec.php\" rel=\"nofollow\">curl_multi_exec()<\/a> reports errors affecting the complete multi stack. A <code>CURLM_OK<\/code> result does not guarantee that every API request succeeded.<\/p>\n<p>Each completed handle must still be checked separately for:<\/p>\n<ul>\n<li>Connection failures reported by <code>curl_error()<\/code><\/li>\n<li>Timeouts and other transfer errors<\/li>\n<li>HTTP error responses such as 404 or 500<\/li>\n<li>Empty or invalid JSON response bodies<\/li>\n<\/ul>\n<p>This distinction is easy to miss. The multi operation may succeed perfectly while one API quietly returns an error page wearing a JSON name tag.<\/p>\n<h2>PHP curl_multi Project Structure<\/h2>\n<p>This example uses plain PHP. It does not need a framework, Composer package, JavaScript library, or database.<\/p>\n<p>The project keeps the HTTP client separate from the page that displays the benchmark. It also includes a local mock API, so the timing test does not depend on an external service.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-plaintext\">php-curl-multi\/\n\u251c\u2500\u2500 config.php\n\u251c\u2500\u2500 mock-api\/\n\u2502 \u2514\u2500\u2500 index.php\n\u251c\u2500\u2500 public\/\n\u2502 \u251c\u2500\u2500 assets\/\n\u2502 \u2502 \u2514\u2500\u2500 style.css\n\u2502 \u2514\u2500\u2500 index.php\n\u251c\u2500\u2500 src\/\n\u2502 \u2514\u2500\u2500 ApiClient.php\n\u2514\u2500\u2500 README.md<\/code><\/pre>\n<ul>\n<li><code>config.php<\/code> contains the API URL, timeouts, and connection limit.<\/li>\n<li><code>mock-api\/index.php<\/code> returns sample JSON responses with controlled delays.<\/li>\n<li><code>src\/ApiClient.php<\/code> sends sequential and concurrent requests.<\/li>\n<li><code>public\/index.php<\/code> runs the benchmark and displays the results.<\/li>\n<li><code>public\/assets\/style.css<\/code> provides the small responsive layout.<\/li>\n<\/ul>\n<h3>Create the Configuration File<\/h3>\n<p>The configuration keeps values that may change between development and production outside the HTTP client class.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">&lt;?php declare(strict_types=1); return [ 'api_base_url' =&gt; rtrim( getenv('DEMO_API_BASE_URL') ?: 'http:\/\/127.0.0.1:8001', '\/' ), 'connect_timeout_ms' =&gt; 1000, 'request_timeout_ms' =&gt; 5000, 'max_concurrent_requests' =&gt; 5,\n];<\/code><\/pre>\n<p>The connection timeout controls how long cURL may spend establishing a connection. The request timeout covers the complete transfer.<\/p>\n<p>The concurrency limit prevents the application from opening an excessive number of connections at once. This demo sends only three requests, but keeping the limit in the configuration makes the client safer to reuse.<\/p>\n<h3>Create the Local Mock API<\/h3>\n<p>The mock API accepts a <code>resource<\/code> query parameter. It returns profile, order, or notification data after a short delay.<\/p>\n<p>Create <code>mock-api\/index.php<\/code> with the following code:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">&lt;?php declare(strict_types=1); header('Content-Type: application\/json; charset=utf-8');\nheader('Cache-Control: no-store'); $resource = $_GET['resource'] ?? ''; $responses = [ 'profile' =&gt; [ 'delay_ms' =&gt; 700, 'data' =&gt; [ 'name' =&gt; 'Maya Chen', 'email' =&gt; 'maya@example.com', 'membership' =&gt; 'Gold', ], ], 'orders' =&gt; [ 'delay_ms' =&gt; 1100, 'data' =&gt; [ 'count' =&gt; 3, 'latest_order' =&gt; '#1048', 'total' =&gt; '$184.50', ], ], 'notifications' =&gt; [ 'delay_ms' =&gt; 900, 'data' =&gt; [ 'unread' =&gt; 4, 'latest' =&gt; 'Your order has been shipped.', ], ],\n]; if (!isset($responses[$resource])) { http_response_code(404); echo json_encode([ 'error' =&gt; 'Unknown API resource.', ], JSON_THROW_ON_ERROR); exit;\n} $response = $responses[$resource]; usleep($response['delay_ms'] * 1000); echo json_encode([ 'resource' =&gt; $resource, 'simulated_delay_ms' =&gt; $response['delay_ms'], 'data' =&gt; $response['data'],\n], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);<\/code><\/pre>\n<p>The delay is intentional. It makes the difference between sequential and concurrent requests easy to see. In a real project, the waiting time would usually come from a remote API, database-backed service, payment gateway, or another server.<\/p>\n<p>The API also returns a proper 404 response for an unknown resource. This gives the client a realistic HTTP error to handle instead of assuming that every response will be successful.<\/p>\n<h2>Create the Reusable PHP API Client<\/h2>\n<p>Create <code>src\/ApiClient.php<\/code>. This class contains both request methods, so the benchmark can compare them under the same timeout and response-handling rules.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">&lt;?php declare(strict_types=1); final class ApiClient\n{ public function __construct( private readonly int $connectTimeoutMs = 1000, private readonly int $requestTimeoutMs = 5000, private readonly int $maxConcurrentRequests = 5 ) { } public function fetchSequential(array $requests): array { $startedAt = hrtime(true); $responses = []; foreach ($requests as $name =&gt; $url) { $handle = $this-&gt;createHandle($url); $body = curl_exec($handle); $responses[$name] = $this-&gt;buildResponse( $handle, $body ); curl_close($handle); } return [ 'duration_ms' =&gt; $this-&gt;elapsedMilliseconds($startedAt), 'responses' =&gt; $responses, ]; } public function fetchConcurrent(array $requests): array { $startedAt = hrtime(true); $multiHandle = curl_multi_init(); $handles = []; curl_multi_setopt( $multiHandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, $this-&gt;maxConcurrentRequests ); try { foreach ($requests as $name =&gt; $url) { $handle = $this-&gt;createHandle($url); $handles[$name] = [ 'handle' =&gt; $handle, ]; $status = curl_multi_add_handle( $multiHandle, $handle ); if ($status !== CURLM_OK) { throw new RuntimeException( curl_multi_strerror($status) ); } } do { $status = curl_multi_exec( $multiHandle, $running ); if ($status !== CURLM_OK) { throw new RuntimeException( curl_multi_strerror($status) ); } if ($running &gt; 0) { $ready = curl_multi_select( $multiHandle, 1.0 ); if ($ready === -1) { usleep(1000); } } } while ($running &gt; 0); $responses = []; foreach ($handles as $name =&gt; $item) { $handle = $item['handle']; $body = curl_multi_getcontent($handle); $responses[$name] = $this-&gt;buildResponse( $handle, $body ); } return [ 'duration_ms' =&gt; $this-&gt;elapsedMilliseconds( $startedAt ), 'responses' =&gt; $responses, ]; } finally { foreach ($handles as $item) { curl_multi_remove_handle( $multiHandle, $item['handle'] ); curl_close($item['handle']); } curl_multi_close($multiHandle); } } private function createHandle(string $url): CurlHandle { $handle = curl_init($url); curl_setopt_array($handle, [ CURLOPT_RETURNTRANSFER =&gt; true, CURLOPT_FOLLOWLOCATION =&gt; false, CURLOPT_CONNECTTIMEOUT_MS =&gt; $this-&gt;connectTimeoutMs, CURLOPT_TIMEOUT_MS =&gt; $this-&gt;requestTimeoutMs, CURLOPT_HTTPHEADER =&gt; [ 'Accept: application\/json' ], CURLOPT_USERAGENT =&gt; 'PHPpot-curl-multi-demo\/1.0', CURLOPT_PROTOCOLS =&gt; CURLPROTO_HTTP | CURLPROTO_HTTPS, ]); return $handle; } private function buildResponse( CurlHandle $handle, string|bool $body ): array { $curlError = curl_error($handle); $statusCode = (int) curl_getinfo( $handle, CURLINFO_RESPONSE_CODE ); $durationMs = round( (float) curl_getinfo( $handle, CURLINFO_TOTAL_TIME ) * 1000, 1 ); if ($body === false || $curlError !== '') { return [ 'ok' =&gt; false, 'status' =&gt; $statusCode, 'duration_ms' =&gt; $durationMs, 'data' =&gt; null, 'error' =&gt; $curlError !== '' ? $curlError : 'The request failed.', ]; } if ($statusCode &lt; 200 || $statusCode &gt;= 300) { return [ 'ok' =&gt; false, 'status' =&gt; $statusCode, 'duration_ms' =&gt; $durationMs, 'data' =&gt; null, 'error' =&gt; 'The API returned HTTP status ' . $statusCode . '.', ]; } try { $data = json_decode( $body, true, 512, JSON_THROW_ON_ERROR ); } catch (JsonException $exception) { return [ 'ok' =&gt; false, 'status' =&gt; $statusCode, 'duration_ms' =&gt; $durationMs, 'data' =&gt; null, 'error' =&gt; 'Invalid JSON response: ' . $exception-&gt;getMessage(), ]; } return [ 'ok' =&gt; true, 'status' =&gt; $statusCode, 'duration_ms' =&gt; $durationMs, 'data' =&gt; $data, 'error' =&gt; null, ]; } private function elapsedMilliseconds( int $startedAt ): float { return round( (hrtime(true) - $startedAt) \/ 1_000_000, 1 ); }\n}<\/code><\/pre>\n<h3>How the Sequential Method Works<\/h3>\n<p><code>fetchSequential()<\/code> creates and executes one handle at a time. The next loop iteration cannot begin until <code>curl_exec()<\/code> returns.<\/p>\n<p>This is useful as the baseline. It shows how much time the application would spend if it made the same API calls without concurrency.<\/p>\n<h3>How the Concurrent Method Works<\/h3>\n<p><code>fetchConcurrent()<\/code> creates the same individual handles, but adds them to one multi handle before execution begins.<\/p>\n<p>The associative request name is kept with each handle. This lets the method return predictable keys such as <code>profile<\/code>, <code>orders<\/code>, and <code>notifications<\/code>, even if the responses finish in a different order.<\/p>\n<p>The <code>finally<\/code> block removes and closes every handle even when an exception occurs. Network code has enough ways to misbehave without leaving cleanup to good luck.<\/p>\n<h3>Validate Every API Response<\/h3>\n<p>The <code>buildResponse()<\/code> method treats transport errors, HTTP errors, and invalid JSON as separate failures. This makes error messages more useful during debugging.<\/p>\n<p>Successful HTTP transport does not guarantee valid JSON. The example uses <code>JSON_THROW_ON_ERROR<\/code> so malformed responses cannot silently become <code>null<\/code>. For more examples, see the PHPpot guide to <a href=\"https:\/\/phppot.com\/php\/json-handling-with-php-how-to-encode-write-parse-decode-and-convert\/\">PHP JSON encode and decode<\/a>.<\/p>\n<p>Each result follows the same structure:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">[ 'ok' =&gt; true, 'status' =&gt; 200, 'duration_ms' =&gt; 703.4, 'data' =&gt; [ \/\/ Decoded API response ], 'error' =&gt; null,\n]<\/code><\/pre>\n<p>A consistent response format keeps the display code simple. It also prevents successful data and error messages from becoming an exciting collection of special cases.<\/p>\n<h2>Build the Benchmark Page<\/h2>\n<p>Create <code>public\/index.php<\/code>. This page defines the three API requests, runs both client methods, and displays the measured time.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php-template\">&lt;?php declare(strict_types=1); require_once dirname(__DIR__) . '\/src\/ApiClient.php'; $config = require dirname(__DIR__) . '\/config.php'; $error = null;\n$sequential = null;\n$concurrent = null; if (!extension_loaded('curl')) { $error = 'The PHP cURL extension is not enabled.';\n} else { $requests = [ 'profile' =&gt; $config['api_base_url'] . '\/?resource=profile', 'orders' =&gt; $config['api_base_url'] . '\/?resource=orders', 'notifications' =&gt; $config['api_base_url'] . '\/?resource=notifications', ]; try { $client = new ApiClient( $config['connect_timeout_ms'], $config['request_timeout_ms'], $config['max_concurrent_requests'] ); $sequential = $client-&gt;fetchSequential($requests); $concurrent = $client-&gt;fetchConcurrent($requests); } catch (Throwable $exception) { $error = $exception-&gt;getMessage(); }\n} function escape(mixed $value): string\n{ return htmlspecialchars( (string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );\n} function seconds(float $milliseconds): string\n{ return number_format( $milliseconds \/ 1000, 2 ) . ' seconds';\n} $timeSaved = $sequential &amp;&amp; $concurrent ? max( 0, $sequential['duration_ms'] - $concurrent['duration_ms'] ) : 0;\n?&gt;\n&lt;!DOCTYPE html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt; &lt;meta charset=\"UTF-8\"&gt; &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" &gt; &lt;title&gt;PHP curl_multi API Benchmark&lt;\/title&gt; &lt;link rel=\"stylesheet\" href=\"assets\/style.css\"&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n&lt;main class=\"page-shell\"&gt; &lt;header class=\"page-header\"&gt; &lt;p class=\"eyebrow\"&gt;PHP cURL benchmark&lt;\/p&gt; &lt;h1&gt;Sequential vs concurrent API requests&lt;\/h1&gt; &lt;p class=\"intro\"&gt; The same three API endpoints are called twice. The first run waits for each response. The second run uses &lt;code&gt;curl_multi&lt;\/code&gt;. &lt;\/p&gt; &lt;\/header&gt; &lt;?php if ($error !== null): ?&gt; &lt;div class=\"message message-error\"&gt; &lt;strong&gt;Unable to run the benchmark.&lt;\/strong&gt; &lt;span&gt;&lt;?= escape($error) ?&gt;&lt;\/span&gt; &lt;\/div&gt; &lt;?php else: ?&gt; &lt;section class=\"benchmark-grid\" aria-label=\"Benchmark results\" &gt; &lt;article class=\"metric-card\"&gt; &lt;span class=\"metric-label\"&gt; Sequential requests &lt;\/span&gt; &lt;strong&gt; &lt;?= escape( seconds($sequential['duration_ms']) ) ?&gt; &lt;\/strong&gt; &lt;small&gt; One request finishes before the next starts. &lt;\/small&gt; &lt;\/article&gt; &lt;article class=\"metric-card metric-card-highlight\" &gt; &lt;span class=\"metric-label\"&gt; Concurrent requests &lt;\/span&gt; &lt;strong&gt; &lt;?= escape( seconds($concurrent['duration_ms']) ) ?&gt; &lt;\/strong&gt; &lt;small&gt; All requests wait for network responses together. &lt;\/small&gt; &lt;\/article&gt; &lt;article class=\"metric-card\"&gt; &lt;span class=\"metric-label\"&gt; Time saved &lt;\/span&gt; &lt;strong&gt; &lt;?= escape(seconds($timeSaved)) ?&gt; &lt;\/strong&gt; &lt;small&gt; Your result will vary slightly between runs. &lt;\/small&gt; &lt;\/article&gt; &lt;\/section&gt; &lt;section class=\"results-section\"&gt; &lt;div class=\"section-heading\"&gt; &lt;div&gt; &lt;p class=\"eyebrow\"&gt; Concurrent response details &lt;\/p&gt; &lt;h2&gt;One result for each API&lt;\/h2&gt; &lt;\/div&gt; &lt;a class=\"button\" href=\"index.php\"&gt; Run benchmark again &lt;\/a&gt; &lt;\/div&gt; &lt;div class=\"response-grid\"&gt; &lt;?php foreach ( $concurrent['responses'] as $name =&gt; $response ): ?&gt; &lt;article class=\"response-card\"&gt; &lt;div class=\"response-title\"&gt; &lt;h3&gt; &lt;?= escape(ucfirst($name)) ?&gt; &lt;\/h3&gt; &lt;span class=\"status &lt;?= $response['ok'] ? 'status-ok' : 'status-error' ?&gt;\"&gt; HTTP &lt;?= escape($response['status']) ?&gt; &lt;\/span&gt; &lt;\/div&gt; &lt;p class=\"response-time\"&gt; Completed in &lt;?= escape( $response['duration_ms'] ) ?&gt; ms &lt;\/p&gt; &lt;?php if ($response['ok']): ?&gt; &lt;pre&gt;&lt;?= escape( json_encode( $response['data']['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ) ?&gt;&lt;\/pre&gt; &lt;?php else: ?&gt; &lt;div class=\"message message-error\" &gt; &lt;?= escape( $response['error'] ) ?&gt; &lt;\/div&gt; &lt;?php endif; ?&gt; &lt;\/article&gt; &lt;?php endforeach; ?&gt; &lt;\/div&gt; &lt;\/section&gt; &lt;?php endif; ?&gt;\n&lt;\/main&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n<h3>Run the Same Request List Twice<\/h3>\n<p>Both methods receive the same associative array of API URLs. This keeps the comparison fair and makes it easy to connect each response to its purpose.<\/p>\n<p>The total duration is measured inside the client. The page subtracts the concurrent duration from the sequential duration to calculate the time saved.<\/p>\n<h3>Escape API Data Before Displaying It<\/h3>\n<p>API responses are external input. Even a trusted service can return unexpected content after a configuration mistake or security incident.<\/p>\n<p>The <code>escape()<\/code> function applies <code>htmlspecialchars()<\/code> before values are printed into the page. JSON shown inside the response cards is escaped as well. Receiving JSON does not make its values automatically safe for HTML output.<\/p>\n<p>The page also checks whether the cURL extension is available. If it is missing, the user sees a useful message instead of PHP introducing the problem with a fatal error.<\/p>\n<h2>Run the PHP curl_multi Project Locally<\/h2>\n<p>Make sure PHP 8.1 or later is installed and the cURL extension is enabled.<\/p>\n<p>You can confirm the extension from the command line:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-bash\">php -m | grep curl<\/code><\/pre>\n<p>If cURL is enabled, the command prints <code>curl<\/code>.<\/p>\n<h3>Start the Mock API<\/h3>\n<p>Open a terminal in the project directory. On macOS or Linux, start the mock API with multiple PHP development-server workers:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-bash\">PHP_CLI_SERVER_WORKERS=4 php -S 127.0.0.1:8001 -t mock-api<\/code><\/pre>\n<p>The mock API is now available at URLs such as:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-plaintext\">http:\/\/127.0.0.1:8001\/?resource=profile\nhttp:\/\/127.0.0.1:8001\/?resource=orders\nhttp:\/\/127.0.0.1:8001\/?resource=notifications<\/code><\/pre>\n<h3>Start the Demo Page<\/h3>\n<p>Open a second terminal in the same project directory:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-bash\">php -S 127.0.0.1:8000 -t public<\/code><\/pre>\n<p>Then open the following URL in a browser:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-plaintext\">http:\/\/127.0.0.1:8000<\/code><\/pre>\n<p>The page runs both benchmarks automatically. With the supplied delays, the sequential test should take about 2.7 seconds. The concurrent test should finish in about 1.1 seconds.<\/p>\n<h3>Important PHP Development Server Caveat<\/h3>\n<p>PHP\u2019s built-in development server uses one worker by default. A single worker can process only one request at a time.<\/p>\n<p>If the benchmark page and mock endpoints are placed on the same single-worker server, the requests may become sequential. They may even wait forever because the current PHP request is trying to call another URL handled by the worker it already occupies.<\/p>\n<p>This is why the example uses two ports and starts the mock API with multiple workers. It is not merely decorative terminal activity.<\/p>\n<h3>Run the Project with XAMPP, MAMP, or Apache<\/h3>\n<p>You can also place the project inside your local web root. Apache normally has multiple workers available, so it can serve the mock API requests concurrently.<\/p>\n<p>For example, the URLs may be:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-plaintext\">Demo page:\nhttp:\/\/localhost\/php-curl-multi\/public\/ Mock API:\nhttp:\/\/localhost\/php-curl-multi\/mock-api\/<\/code><\/pre>\n<p>Update <code>api_base_url<\/code> in <code>config.php<\/code>:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">'api_base_url' =&gt; 'http:\/\/localhost\/php-curl-multi\/mock-api',<\/code><\/pre>\n<p>You can also set the API URL through an environment variable:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-bash\">export DEMO_API_BASE_URL=\"http:\/\/localhost\/php-curl-multi\/mock-api\"<\/code><\/pre>\n<p>If the concurrent result is almost as slow as the sequential result, check the mock API server first. The most common cause is a local server that still processes the API requests one at a time.<\/p>\n<h2>When Concurrent API Requests Improve Performance<\/h2>\n<p><code>curl_multi<\/code> works best when a PHP page needs several independent HTTP responses before it can continue.<\/p>\n<p>Good examples include:<\/p>\n<ul>\n<li>Loading profile, order, and notification data from separate services<\/li>\n<li>Collecting prices from several supplier APIs<\/li>\n<li>Checking the status of multiple remote servers<\/li>\n<li>Fetching reports from independent endpoints<\/li>\n<li>Sending the same webhook payload to several destinations<\/li>\n<\/ul>\n<p>The important word is <em>independent<\/em>. If one request needs data returned by another request, those two calls cannot start together.<\/p>\n<h3>Independent Requests Can Run Concurrently<\/h3>\n<pre class=\"prettyprint\"><code class=\"language-php\">$requests = [ 'profile' =&gt; $profileUrl, 'orders' =&gt; $ordersUrl, 'notifications' =&gt; $notificationsUrl,\n]; $results = $client-&gt;fetchConcurrent($requests);<\/code><\/pre>\n<p>None of these URLs depends on another response. They are good candidates for <code>curl_multi<\/code>.<\/p>\n<h3>Dependent Requests Must Keep Their Order<\/h3>\n<pre class=\"prettyprint\"><code class=\"language-php\">$loginResponse = sendRequest($loginUrl); $accessToken = $loginResponse['access_token']; $profileResponse = sendAuthenticatedRequest( $profileUrl, $accessToken\n);<\/code><\/pre>\n<p>The profile request needs the access token returned by the login request. Starting both calls together would only help them fail faster.<\/p>\n<h3>When curl_multi Will Not Help<\/h3>\n<table>\n<thead>\n<tr>\n<th>Situation<\/th>\n<th>Why curl_multi does not solve it<\/th>\n<\/tr>\n<\/thead>\n<tbody readability=\"9.5\">\n<tr readability=\"2\">\n<td>One slow API request<\/td>\n<td>There is no other network wait time to overlap.<\/td>\n<\/tr>\n<tr readability=\"5\">\n<td>CPU-heavy PHP calculations<\/td>\n<td>curl_multi manages network transfers, not PHP computation.<\/td>\n<\/tr>\n<tr readability=\"4\">\n<td>Requests that depend on earlier responses<\/td>\n<td>Later calls cannot start until the required data exists.<\/td>\n<\/tr>\n<tr readability=\"4\">\n<td>An API with strict rate limits<\/td>\n<td>More simultaneous requests may trigger throttling.<\/td>\n<\/tr>\n<tr readability=\"4\">\n<td>A server that handles one request at a time<\/td>\n<td>The remote side still processes the calls sequentially.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>The Slowest Request Still Sets the Pace<\/h3>\n<p>Concurrent requests reduce the total from approximately the sum of all response times to approximately the longest response time.<\/p>\n<p>For three requests taking one, two, and five seconds:<\/p>\n<ul>\n<li>Sequential time is approximately eight seconds.<\/li>\n<li>Concurrent time is approximately five seconds.<\/li>\n<\/ul>\n<p>The five-second API remains a five-second API. <code>curl_multi<\/code> simply stops the other requests from waiting in line behind it.<\/p>\n<h3>Do Not Send Hundreds of Requests at Once<\/h3>\n<p>Concurrency needs a limit. Opening too many connections can increase memory use, overload the remote service, or cause HTTP 429 responses.<\/p>\n<p>The example limits the complete multi handle with:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">curl_multi_setopt( $multiHandle, CURLMOPT_MAX_TOTAL_CONNECTIONS, 5\n);<\/code><\/pre>\n<p>Choose the limit based on the API documentation, server capacity, and rate policy. Five or ten concurrent requests may be reasonable for one service. Five hundred is usually a creative way to meet its security team.<\/p>\n<h2>Common PHP curl_multi Errors and Fixes<\/h2>\n<h3>curl_multi_exec() Returns CURLM_OK, but a Request Failed<\/h3>\n<p><code>CURLM_OK<\/code> means the multi stack operated correctly. It does not mean every individual transfer succeeded.<\/p>\n<p>Check each handle after execution:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">$curlError = curl_error($handle); $statusCode = (int) curl_getinfo( $handle, CURLINFO_RESPONSE_CODE\n); if ($curlError !== '') { echo 'cURL error: ' . $curlError;\n} if ($statusCode &lt; 200 || $statusCode &gt;= 300) { echo 'HTTP error: ' . $statusCode;\n}<\/code><\/pre>\n<p>A request can time out, fail DNS lookup, or return HTTP 500 while the multi handle itself remains perfectly content.<\/p>\n<h3>curl_multi_select() Returns -1<\/h3>\n<p>Some libcurl builds may return <code>-1<\/code> when no file descriptor is ready. Add a short sleep before continuing the loop:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">$ready = curl_multi_select($multiHandle, 1.0); if ($ready === -1) { usleep(1000);\n}<\/code><\/pre>\n<p>This prevents the loop from repeatedly checking the connections and consuming unnecessary CPU.<\/p>\n<h3>The Concurrent Version Is Not Faster<\/h3>\n<p>Check whether the requests are truly independent and whether the API server can process more than one request at a time.<\/p>\n<p>During local testing, a single-worker PHP development server is a common cause. Use multiple API workers or run the mock endpoints through Apache or Nginx.<\/p>\n<p>Concurrency may also provide little improvement when:<\/p>\n<ul>\n<li>The API responses are already extremely fast.<\/li>\n<li>Most of the time is spent processing data after download.<\/li>\n<li>The remote server queues requests from the same client.<\/li>\n<li>The API applies a low concurrency limit.<\/li>\n<\/ul>\n<h3>A Request Never Finishes<\/h3>\n<p>Every handle should have both a connection timeout and a total timeout:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">curl_setopt_array($handle, [ CURLOPT_CONNECTTIMEOUT_MS =&gt; 1000, CURLOPT_TIMEOUT_MS =&gt; 5000,\n]);<\/code><\/pre>\n<p>The connection timeout covers DNS lookup and connection setup. The total timeout limits the complete request.<\/p>\n<p>Without these values, one unresponsive endpoint can keep the entire group waiting. Concurrent does not mean immortal.<\/p>\n<h3>The API Returns HTTP 429<\/h3>\n<p>HTTP 429 means the service is rate-limiting the client. Reduce the concurrency limit and inspect the response headers for retry information.<\/p>\n<p>A production client may need to:<\/p>\n<ul>\n<li>Respect the API\u2019s documented request limit.<\/li>\n<li>Wait for the duration given by a <code>Retry-After<\/code> header.<\/li>\n<li>Retry only safe requests.<\/li>\n<li>Use exponential backoff instead of retrying immediately.<\/li>\n<\/ul>\n<p>Do not treat retries as permission to hammer the same endpoint with greater determination.<\/p>\n<h3>json_decode() Returns null<\/h3>\n<p>A response may contain invalid JSON, an empty body, or an HTML error page. Use <code>JSON_THROW_ON_ERROR<\/code> to make the failure visible:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">try { $data = json_decode( $body, true, 512, JSON_THROW_ON_ERROR );\n} catch (JsonException $exception) { $error = 'Invalid JSON response: ' . $exception-&gt;getMessage();\n}<\/code><\/pre>\n<p>Check the HTTP status before decoding. A server returning an HTML 500 page is not a JSON parsing mystery. It is an HTTP error wearing the wrong outfit.<\/p>\n<h3>Responses Are Matched to the Wrong Request<\/h3>\n<p>Do not depend on completion order. A fast endpoint may finish before a request that was added earlier.<\/p>\n<p>Store each handle with a stable name:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">foreach ($requests as $name =&gt; $url) { $handle = curl_init($url); $handles[$name] = $handle; curl_multi_add_handle( $multiHandle, $handle );\n}<\/code><\/pre>\n<p>When reading the results, use the same name as the response key. This keeps profile data attached to <code>profile<\/code>, even when notifications finish first.<\/p>\n<h2>Security Considerations<\/h2>\n<p>Concurrent requests do not introduce a completely new security model, but they can multiply the effect of a bad URL, missing timeout, or leaked credential. Apply the same controls to every handle.<\/p>\n<h3>Do Not Accept Arbitrary Request URLs<\/h3>\n<p>The example builds its URLs from a trusted configuration value. Do not pass a URL from <code>$_GET<\/code>, <code>$_POST<\/code>, or another untrusted source directly to <code>curl_init()<\/code>.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">\/\/ Unsafe\n$url = $_GET['url'] ?? ''; $handle = curl_init($url);<\/code><\/pre>\n<p>This can create a server-side request forgery vulnerability. An attacker may try to access internal services, cloud metadata endpoints, or private network addresses through your server.<\/p>\n<p>For a production integration, keep API endpoints in configuration or restrict requests to an explicit list of trusted hosts.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">function isAllowedApiUrl( string $url, array $allowedHosts\n): bool { $parts = parse_url($url); if ( !isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'https' ) { return false; } return in_array( strtolower($parts['host']), $allowedHosts, true );\n} $allowedHosts = [ 'api.example.com', 'payments.example.com',\n]; if (!isAllowedApiUrl($url, $allowedHosts)) { throw new InvalidArgumentException( 'The API URL is not allowed.' );\n}<\/code><\/pre>\n<p>A hostname allowlist is only one layer. Applications that fetch user-influenced URLs should also protect against hostnames resolving to private IP addresses and DNS rebinding.<\/p>\n<h3>Restrict Supported Protocols<\/h3>\n<p>Limit cURL to the protocols the application actually needs:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">CURLOPT_PROTOCOLS =&gt; CURLPROTO_HTTP | CURLPROTO_HTTPS,<\/code><\/pre>\n<p>This prevents an unexpected URL from switching to protocols such as FTP or local file access.<\/p>\n<h3>Handle Redirects Carefully<\/h3>\n<p>The example disables automatic redirects:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">CURLOPT_FOLLOWLOCATION =&gt; false,<\/code><\/pre>\n<p>A trusted public URL can redirect to an untrusted or private address. If redirects are required, validate every destination and set a small redirect limit.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">CURLOPT_FOLLOWLOCATION =&gt; true,\nCURLOPT_MAXREDIRS =&gt; 3,\nCURLOPT_REDIR_PROTOCOLS =&gt; CURLPROTO_HTTP | CURLPROTO_HTTPS,<\/code><\/pre>\n<p>Protocol restriction alone does not stop redirects to private HTTP addresses. The destination still needs validation.<\/p>\n<h3>Keep TLS Verification Enabled<\/h3>\n<p>For HTTPS APIs, PHP cURL verifies the certificate and hostname by default. Do not disable these checks to silence a certificate error.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">\/\/ Do not use these settings in production.\nCURLOPT_SSL_VERIFYPEER =&gt; false,\nCURLOPT_SSL_VERIFYHOST =&gt; 0,<\/code><\/pre>\n<p>Fix the server certificate or local certificate store instead. Disabling verification allows a network attacker to intercept API credentials and responses.<\/p>\n<h3>Keep API Credentials Outside the Source Code<\/h3>\n<p>Read tokens from environment variables or a secret manager:<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">$apiToken = getenv('API_TOKEN'); if (!$apiToken) { throw new RuntimeException( 'The API token is not configured.' );\n} curl_setopt($handle, CURLOPT_HTTPHEADER, [ 'Accept: application\/json', 'Authorization: Bearer ' . $apiToken,\n]);<\/code><\/pre>\n<p>Do not print authorization headers in browser errors or write complete tokens into application logs.<\/p>\n<h3>Limit Time, Connections, and Response Size<\/h3>\n<p>Timeouts and connection limits protect application resources when an API becomes slow or unavailable. For APIs that may return large files or untrusted content, also enforce a maximum response size.<\/p>\n<p>Without limits, five concurrent requests can become five simultaneous ways to exhaust memory.<\/p>\n<h3>Escape Response Data<\/h3>\n<p>JSON values are not safe HTML merely because they arrived from an API. Escape values with <code>htmlspecialchars()<\/code> before placing them on a page.<\/p>\n<p>This matters even for a trusted service. Accounts can be compromised, stored data can contain markup, and APIs occasionally return surprises that were not included in the integration meeting.<\/p>\n<h2>Developer FAQ<\/h2>\n<h3>Is PHP curl_multi truly parallel?<\/h3>\n<p><code>curl_multi<\/code> performs concurrent network transfers. Several requests can make progress during the same period, but your PHP code is not executing in multiple CPU threads.<\/p>\n<p>For HTTP requests, concurrent is the more accurate term. Developers often search for \u201cparallel cURL requests,\u201d so both descriptions are commonly used.<\/p>\n<h3>Does curl_multi_exec() run in the background?<\/h3>\n<p>No. The PHP script still waits until the multi loop finishes. The difference is that it waits for several active transfers together instead of completing them one by one.<\/p>\n<p>If work must continue after the web request ends, use a queue, worker, scheduled job, or another background-processing system.<\/p>\n<h3>Can curl_multi send POST requests?<\/h3>\n<p>Yes. Each handle can have its own HTTP method, headers, and request body.<\/p>\n<pre class=\"prettyprint\"><code class=\"language-php\">$handle = curl_init($url); curl_setopt_array($handle, [ CURLOPT_POST =&gt; true, CURLOPT_POSTFIELDS =&gt; json_encode( $payload, JSON_THROW_ON_ERROR ), CURLOPT_HTTPHEADER =&gt; [ 'Content-Type: application\/json', 'Accept: application\/json', ], CURLOPT_RETURNTRANSFER =&gt; true,\n]);<\/code><\/pre>\n<p>The configured handle can then be added with <code>curl_multi_add_handle()<\/code>. See the <a href=\"https:\/\/phppot.com\/php\/php-curl-post\/\">PHP cURL POST example<\/a> for more details about sending form data and JSON request bodies.<\/p>\n<h3>Can GET and POST requests be mixed in one multi handle?<\/h3>\n<p>Yes. Every easy handle keeps its own options. One handle can send GET, another can send POST, and another can include authentication headers.<\/p>\n<p>The multi handle manages when their network transfers progress. It does not require every request to use the same method or destination.<\/p>\n<h3>Are responses returned in the same order as the URLs?<\/h3>\n<p>Requests may finish in any order. Do not assume that the first completed response belongs to the first URL.<\/p>\n<p>Store each handle with a stable associative key and return results using that key. The example uses <code>profile<\/code>, <code>orders<\/code>, and <code>notifications<\/code>.<\/p>\n<h3>How many concurrent requests should PHP send?<\/h3>\n<p>There is no universal safe number. It depends on the remote API, response size, available memory, connection limits, and rate policy.<\/p>\n<p>Start with a small value such as five. Measure the result and respect any limits documented by the API provider. More concurrency is not automatically more performance.<\/p>\n<h3>Why use curl_multi_select() instead of usleep() in every loop?<\/h3>\n<p><code>curl_multi_select()<\/code> waits for actual network activity. A fixed sleep may pause longer than necessary or wake repeatedly when no transfer can make progress.<\/p>\n<p>A short <code>usleep()<\/code> is used only when <code>curl_multi_select()<\/code> returns <code>-1<\/code>.<\/p>\n<h3>Does curl_multi make a slow API faster?<\/h3>\n<p>No. It reduces unnecessary waiting between independent requests. The complete group still depends on its slowest request.<\/p>\n<p>If one API is consistently slow, improve that service, add appropriate caching, request less data, or move nonessential work to a background process.<\/p>\n<h3>Can curl_multi be used for file downloads?<\/h3>\n<p>Yes. It can download several files concurrently. For large files, write each response directly to a file instead of keeping every body in memory.<\/p>\n<p>Also limit concurrency and maximum file size. A small JSON response and a two-gigabyte archive have very different opinions about available memory.<\/p>\n<h2>Final Takeaway<\/h2>\n<p>PHP <code>curl_multi<\/code> is useful when one operation needs responses from several independent APIs. It starts the transfers together, waits efficiently for network activity, and reduces total waiting time to roughly the duration of the slowest request.<\/p>\n<p>The important parts are not limited to calling <code>curl_multi_exec()<\/code>. A reliable implementation should also:<\/p>\n<ul>\n<li>Use <code>curl_multi_select()<\/code> to avoid a busy loop.<\/li>\n<li>Set connection and total-request timeouts.<\/li>\n<li>Check cURL errors for every handle.<\/li>\n<li>Validate HTTP status codes separately.<\/li>\n<li>Handle invalid JSON responses.<\/li>\n<li>Keep responses associated with stable request names.<\/li>\n<li>Limit the number of concurrent connections.<\/li>\n<li>Restrict URLs, protocols, and redirects.<\/li>\n<\/ul>\n<p>Concurrency is not helpful for every problem. It will not accelerate CPU-heavy PHP code, remove API rate limits, or make a single slow endpoint respond faster. But when several independent HTTP calls are holding up a page, it can remove a surprising amount of avoidable waiting.<\/p>\n<h2>Download the PHP curl_multi Project<\/h2>\n<p>The downloadable ZIP contains the complete working project, including the reusable API client, local mock API, benchmark page, stylesheet, configuration, and setup instructions.<\/p>\n<p><a class=\"download\" href=\"https:\/\/phppot.com\/downloads\/php\/php-curl-multi.zip\">Download the PHP curl_multi concurrent API requests project<\/a><\/p>\n<p>Extract the ZIP, follow the instructions in <code>README.md<\/code>, and run the mock API and demo page on separate local ports.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Calling one API from PHP is simple. Calling three APIs one after another is also simple, but it can make the user wait far longer than necessary. Each request gets its own private turn. Very polite. Not very fast. Suppose a page needs a customer profile, recent orders, and notifications. If those APIs take 700, [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":137771,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[65],"tags":[],"class_list":["post-137770","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-php-updates"],"_links":{"self":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/137770","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/comments?post=137770"}],"version-history":[{"count":0,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/137770\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media\/137771"}],"wp:attachment":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media?parent=137770"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/categories?post=137770"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/tags?post=137770"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}