After curl_multi_exec(), read completed-transfer messages with curl_multi_info_read(). The message’s result value is the error code for that individual request. curl_multi_errno() reports a failure of the multi stack itself, not the result of every transfer.
Last updated: October 28, 2026.
<?php
$multi = curl_multi_init();
$handles = [];
foreach (['https://example.com/', 'http://127.0.0.1:1/'] as $url) {
$handle = curl_init($url);
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 10,
]);
curl_multi_add_handle($multi, $handle);
$handles[] = $handle;
}
do {
$multiCode = curl_multi_exec($multi, $running);
if ($multiCode !== CURLM_OK) {
throw new RuntimeException(curl_multi_strerror($multiCode));
}
while ($info = curl_multi_info_read($multi)) {
$handle = $info['handle'];
if ($info['result'] !== CURLE_OK) {
error_log(curl_strerror($info['result']));
}
curl_multi_remove_handle($multi, $handle);
curl_close($handle);
}
if ($running) {
curl_multi_select($multi, 1.0);
}
} while ($running);
curl_multi_close($multi);Drain the message queue every time the multi loop advances. Each returned message contains the completed handle and its own result, letting the application associate an error with the correct URL or request identifier.
Read the per-transfer result before cleanup
The curl_multi_info_read() documentation defines three important fields: msg, result, and handle. A completed message uses CURLMSG_DONE; result is a CURLE_* value such as CURLE_COULDNT_CONNECT, or CURLE_OK for a successful transfer.
Read the message before calling curl_multi_remove_handle(). The returned transfer information does not survive removal. Use curl_strerror($info["result"]) for a generic description, and capture request context separately so logs identify which operation failed.
Separate transport errors from HTTP responses
The curl_errno() documentation notes that a multi-executed handle can report zero until its completion message has been read. Also, an HTTP 404 or 500 is normally a completed HTTP transfer, not a cURL transport error. Inspect CURLINFO_RESPONSE_CODE when the application needs to classify HTTP status codes.
Set bounded connection and total timeouts, log failures without secrets, and retry only idempotent operations or requests protected by an idempotency key. Continue with diagnosing cURL timeout error 28, handling cURL option failures, and preventing duplicate API operations.