PHP curl_multi_exec Function
last modified November 14, 2026
The PHP curl_multi_exec function executes multiple cURL handles in
parallel. It's used to perform multiple HTTP requests simultaneously, improving
performance when fetching data from multiple sources.
Basic Definition
The curl_multi_exec function runs the sub-connections of the
current cURL multi handle. It processes each handle asynchronously, enabling
parallel requests. The function returns a cURL code defined in cURL.h.
Syntax: curl_multi_exec(CurlMultiHandle $multi_handle, int &$still_running): int.
The $still_running parameter is filled with the number of still
running handles. The function should be called repeatedly until all handles
complete.
Basic Parallel Requests
This example demonstrates making two parallel GET requests to different URLs.
<?php
declare(strict_types=1);
// Create multiple cURL handles
$ch1 = curl_init();
$ch2 = curl_init();
curl_setopt($ch1, CURLOPT_URL, "https://jsonplaceholder.typicode.com/posts/1");
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_URL, "https://jsonplaceholder.typicode.com/comments/1");
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
// Create the multi cURL handle
$mh = curl_multi_init();
// Add the handles
curl_multi_add_handle($mh, $ch1);
curl_multi_add_handle($mh, $ch2);
// Execute the handles
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
// Get the responses
$response1 = curl_multi_getcontent($ch1);
$response2 = curl_multi_getcontent($ch2);
// Close the handles
curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_close($mh);
echo "Post 1: " . $response1 . "\n\n";
echo "Comment 1: " . $response2 . "\n";
This code creates two cURL handles for different API endpoints. We add them to a multi handle and execute them in parallel. The loop continues until all requests complete. Finally, we retrieve and display the responses.
$ php basic_parallel.php
Post 1: {
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Comment 1: {
"postId": 1,
"id": 1,
"name": "id labore ex et quam laborum",
"email": "Eliseo@gardner.biz",
"body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
}
Handling Multiple Responses
This example shows how to properly handle responses from multiple parallel requests.
<?php
declare(strict_types=1);
$urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/comments/1",
"https://jsonplaceholder.typicode.com/albums/1"
];
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$handles[$i] = curl_init();
curl_setopt($handles[$i], CURLOPT_URL, $url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $handles[$i]);
}
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
$responses = [];
foreach ($handles as $i => $handle) {
$responses[$i] = curl_multi_getcontent($handle);
curl_multi_remove_handle($mh, $handle);
}
curl_multi_close($mh);
foreach ($responses as $i => $response) {
$data = json_decode($response, true);
echo "Response $i: " . ($data['title'] ?? $data['name']) . "\n";
}
We create multiple cURL handles from an array of URLs. After parallel execution, we collect all responses in an array. The example demonstrates proper resource cleanup and shows how to process multiple responses efficiently.
$ php multiple_responses.php Response 0: sunt aut facere repellat provident occaecati excepturi optio reprehenderit Response 1: id labore ex et quam laborum Response 2: quidem molestiae enim
Error Handling in Parallel Requests
This example demonstrates proper error handling for parallel cURL requests.
<?php
declare(strict_types=1);
$urls = [
"https://httpbin.org/get",
"https://nonexistent.invalid/api",
"https://httpbin.org/status/500",
"https://httpbin.org/delay/5",
];
$mh = curl_multi_init();
foreach ($urls as $i => $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
curl_setopt($ch, CURLOPT_PRIVATE, (string) $i);
curl_multi_add_handle($mh, $ch);
}
$results = [];
do {
$status = curl_multi_exec($mh, $running);
if ($status !== CURLM_OK) {
echo "Multi handle error: " . curl_multi_strerror($status) . "\n";
break;
}
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$i = (int) curl_getinfo($ch, CURLINFO_PRIVATE);
if ($info['result'] !== CURLE_OK) {
$results[$i] = "Error: " . curl_strerror($info['result']);
} else {
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$results[$i] = $code === 200 ? "Success" : "HTTP error $code";
}
curl_multi_remove_handle($mh, $ch);
}
if ($running > 0) {
curl_multi_select($mh);
}
} while ($running > 0);
curl_multi_close($mh);
ksort($results);
foreach ($results as $i => $result) {
echo "{$urls[$i]}: $result\n";
}
This example checks the outcome of every request. One URL succeeds, one has a host name that does not exist, one returns an HTTP error, and one takes longer than the two-second timeout.
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
...
if ($info['result'] !== CURLE_OK) {
$results[$i] = "Error: " . curl_strerror($info['result']);
}
In the multi interface, the result of each transfer is delivered by
curl_multi_info_read. Its result element holds the
cURL error code of the finished transfer, and curl_strerror
turns the code into a message. The curl_error and
curl_errno functions are not reliable here: they belong to
curl_exec and report no error for a handle run by a multi
handle, even when the transfer failed.
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $results[$i] = $code === 200 ? "Success" : "HTTP error $code";
A transfer that completed without a cURL error can still return an HTTP error status, so we also check the response code.
$status = curl_multi_exec($mh, $running);
if ($status !== CURLM_OK) {
echo "Multi handle error: " . curl_multi_strerror($status) . "\n";
break;
}
The return value of curl_multi_exec itself reports errors of
the multi handle, not of the individual transfers. We convert it to a message
with curl_multi_strerror. The CURLOPT_PRIVATE option
stores the index of each URL in its handle, so the results can be printed in
the original order.
$ php error_handling.php https://httpbin.org/get: Success https://nonexistent.invalid/api: Error: Could not resolve hostname https://httpbin.org/status/500: HTTP error 500 https://httpbin.org/delay/5: Error: Timeout was reached
Controlling Concurrency
This example shows how to control the number of parallel requests using a queue.
<?php
declare(strict_types=1);
$urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3",
"https://jsonplaceholder.typicode.com/posts/4",
"https://jsonplaceholder.typicode.com/posts/5"
];
$maxConcurrent = 2; // Maximum parallel requests
$mh = curl_multi_init();
$activeHandles = [];
$allResponses = [];
$currentIndex = 0;
while ($currentIndex < count($urls) || count($activeHandles) > 0) {
// Add new requests if under limit
while (count($activeHandles) < $maxConcurrent && $currentIndex < count($urls)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $urls[$currentIndex]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $ch);
$activeHandles[$currentIndex] = $ch;
$currentIndex++;
}
// Execute the current batch
$running = null;
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
// Process completed requests
foreach ($activeHandles as $i => $ch) {
$info = curl_getinfo($ch);
if ($info['http_code'] == 200) {
$allResponses[$i] = json_decode(curl_multi_getcontent($ch), true)['title'];
} else {
$allResponses[$i] = "Error for URL $i";
}
curl_multi_remove_handle($mh, $ch);
unset($activeHandles[$i]);
}
}
curl_multi_close($mh);
print_r($allResponses);
This advanced example implements a queue system to control concurrency. We process URLs in batches of 2, maintaining optimal performance without overwhelming servers. Completed requests are processed and removed before starting new ones.
$ php concurrency_control.php
Array
(
[0] => sunt aut facere repellat provident occaecati excepturi optio reprehenderit
[1] => qui est esse
[2] => ea molestias quasi exercitationem repellat qui ipsa sit aut
[3] => eum et est occaecati
[4] => nesciunt quas odio
)
Processing Responses as They Complete
This example demonstrates processing responses immediately as they complete.
<?php
declare(strict_types=1);
$urls = [
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/comments/1",
"https://jsonplaceholder.typicode.com/albums/1",
"https://jsonplaceholder.typicode.com/photos/1",
"https://jsonplaceholder.typicode.com/todos/1"
];
$mh = curl_multi_init();
$handles = [];
foreach ($urls as $i => $url) {
$handles[$i] = curl_init();
curl_setopt($handles[$i], CURLOPT_URL, $url);
curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $handles[$i]);
}
$completed = 0;
$total = count($urls);
do {
$status = curl_multi_exec($mh, $running);
if ($status === CURLM_OK) {
// Check for completed requests
while ($info = curl_multi_info_read($mh)) {
$handle = $info['handle'];
$index = array_search($handle, $handles, true);
if ($info['result'] === CURLE_OK) {
$data = json_decode(curl_multi_getcontent($handle), true);
echo "Completed $index: " . ($data['title'] ?? $data['name']) . "\n";
} else {
echo "Failed $index: " . curl_error($handle) . "\n";
}
curl_multi_remove_handle($mh, $handle);
$completed++;
}
}
if ($running) {
curl_multi_select($mh, 0.1);
}
} while ($running > 0 || $completed < $total);
curl_multi_close($mh);
This example processes responses as soon as they become available, rather than
waiting for all requests to complete. We use curl_multi_info_read
to detect completed requests. This approach is useful for processing large
numbers of requests with varying response times. The output shows that the
responses arrived in a different order from the one in which the requests
were started.
$ php streaming_processing.php Completed 2: quidem molestiae enim Completed 1: id labore ex et quam laborum Completed 0: sunt aut facere repellat provident occaecati excepturi optio reprehenderit Completed 4: delectus aut autem Completed 3: accusamus beatae ad facilis cum similique qui sunt
Best Practices
Keep the following points in mind when running parallel requests.
- Limit Concurrency: Control parallel requests to avoid overwhelming servers.
- Error Handling: Read each transfer's result with
curl_multi_info_readand check the HTTP status code. - Resource Cleanup: Remove finished handles with
curl_multi_remove_handle; since PHP 8.0 handles are objects freed automatically, andcurl_closeis deprecated in PHP 8.5. - Timeouts: Set CURLOPT_TIMEOUT for each handle.
- Memory Management: Process responses as they complete for large batches.
Source
PHP curl_multi_exec Documentation
This tutorial covered the PHP curl_multi_exec function with practical
examples showing parallel request execution and response handling. See also the
curl_multi_getcontent and
curl_multi_remove_handle tutorials.
Author
List all PHP cURL tutorials.