guzzle/guzzle · error · GuzzleHttp\Exception\RequestException
Unable to add the cURL handle to the cURL multi handler
Error message
Unable to add the cURL handle to the cURL multi handler: %s (%d).
What it means
Thrown from addHandleToMulti() when curl_multi_add_handle() returns a code other than CURLM_OK while attaching an easy handle to the multi handle during an async transfer. Guzzle wraps it as a RequestException so the promise rejects instead of running the transfer in an undefined state. The message embeds curl_multi_strerror() text and the numeric libcurl result code.
Solutions
- Inspect the numeric code and strerror in the message: CURLM_OUT_OF_MEMORY is transient, CURLM_ADDED_ALREADY points to a handle reused before removal.
- If using a custom handle_factory, ensure it returns a fresh, never-attached easy handle on every call.
- Bound concurrency with max_host_connections/max_total_connections and free handles promptly to avoid memory pressure.
- Upgrade libcurl/PHP ext-curl if CURLM_INTERNAL_ERROR recurs on stock handles.
Example fix
// before
$promise = $handler->addRequest($easy);
$promise->wait(); // RequestException: Unable to add the cURL handle ... (CURLM_OUT_OF_MEMORY)
// after - bounded caps + retry on transient multi errors
use GuzzleHttp\Exception\RequestException;
try {
$promise->wait();
} catch (RequestException $e) {
if (str_contains($e->getMessage(), 'OUT_OF_MEMORY')) {
// shed load, free handles, then retry once
}
throw $e;
} Defensive patterns
Strategy: try-catch
Try / catch
use GuzzleHttp\Exception\RequestException;
try {
$promise->wait();
} catch (RequestException $e) {
// $e->getMessage() ends with "... (CURLM_<CODE>)."
if (str_contains($e->getMessage(), 'OUT_OF_MEMORY')) {
// transient: shed load, free handles, retry once
}
throw $e;
} Prevention
- Bound concurrency with max_host_connections / max_total_connections
- Return fresh, never-attached handles from any custom handle_factory
- Never reuse an easy handle after close or removal
- Upgrade libcurl/ext-curl for stability fixes
When it happens
Trigger: Awaiting any async request on a CurlMultiHandler (e.g., $client->requestAsync(...)->wait(), or $handler->addRequest(...)) where the native curl_multi_add_handle call fails. Possible libcurl results include CURLM_OUT_OF_MEMORY, CURLM_BAD_EASY_HANDLE, CURLM_ADDED_ALREADY, and CURLM_INTERNAL_ERROR.
Common situations: Memory exhaustion under very high concurrency; an easy handle accidentally added twice (custom handle_factory returning a pooled/already-attached handle); a handle reused after it was closed; a buggy or very old libcurl returning CURLM_INTERNAL_ERROR.
Related errors
- cURL or higher is required by the cURL handler; is…
- Required multiplexing for HTTP/3 needs libcurl 8.13.0 or…
- Unable to apply the cURL multi option
- A cURL share handle cannot be provided when transport…
- A cURL share handle is required when transport sharing is…
AI-assisted analysis of guzzle/guzzle@d1cbca7697 (2026-08-06).
Data as JSON: /api/errors/108bfcb7f39d766b.
Report an issue: GitHub.
Appendix: source
Thrown at src/Handler/CurlMultiHandler.php:656
): void {
$this->isolateFromForeignActiveProxyTunnel($easy);
$multiHandle = $this->getMultiHandle();
// Unqualified curl_multi_add_handle so the test bootstrap shadow can
// override the result.
$result = curl_multi_add_handle($multiHandle, $easy->handle);
if (\CURLM_OK !== $result) {
if (\PHP_VERSION_ID < 80226 || (\PHP_VERSION_ID >= 80300 && \PHP_VERSION_ID < 80314)) {
// Before PHP 8.2.26 and 8.3.14, ext-curl kept the easy handle
// in its multi bookkeeping even when the native add failed
// (https://github.com/php/php-src/pull/16302); remove it so
// the handle can be disposed safely.
\curl_multi_remove_handle($multiHandle, $easy->handle);
}
throw new RequestException(\sprintf('Unable to add the cURL handle to the cURL multi handler: %s (%d).', (string) \curl_multi_strerror($result), $result), $easy->request);
}
$this->markProxyTunnelActive($id, $easy);
if (isset($this->handles[$id])) {
$this->handles[$id]['attached'] = true;
}
}
private function isolateFromForeignActiveProxyTunnel(
#[\SensitiveParameter]
EasyHandle $easy
): void {
$signature = $easy->proxyTunnelSignature;
if ($signature === null || $this->activeProxyTunnelSignatures === []) {
return;
}View on GitHub (pinned to d1cbca7697)