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

  1. 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.
  2. If using a custom handle_factory, ensure it returns a fresh, never-attached easy handle on every call.
  3. Bound concurrency with max_host_connections/max_total_connections and free handles promptly to avoid memory pressure.
  4. 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

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


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)