PHP curl_multi_remove_handle Function
last modified November 14, 2026
The PHP curl_multi_remove_handle function detaches a cURL handle
from a multi handle. In this article we show when to call it, how it lets us
reuse handles, cancel slow requests and keep a fixed number of transfers
running.
The examples were run with PHP 8.5 and the cURL extension against the httpbin.org testing service.
Basic definition
The function has the following signature:
curl_multi_remove_handle(CurlMultiHandle $multi_handle, CurlHandle $handle): int
It removes the easy handle $handle from
$multi_handle. If the transfer of that handle is still in
progress, it is stopped. The easy handle itself stays valid: we can still
read its content and information, change its options, run it again, or add it
to a multi handle later.
The function returns one of the CURLM_* constants;
CURLM_OK (0) means success. The
curl_multi_strerror function converts the code to a message.
Removing finished handles
The most common use is cleaning up after the transfers have finished.
<?php
declare(strict_types=1);
$mh = curl_multi_init();
$ch1 = curl_init('https://httpbin.org/get?id=1');
$ch2 = curl_init('https://httpbin.org/get?id=2');
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch2, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $ch1);
curl_multi_add_handle($mh, $ch2);
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
foreach ([$ch1, $ch2] as $ch) {
$data = json_decode(curl_multi_getcontent($ch), true);
echo "id {$data['args']['id']} received\n";
$status = curl_multi_remove_handle($mh, $ch);
echo 'remove: ', curl_multi_strerror($status), "\n";
}
curl_multi_close($mh);
We run two requests in parallel. After the loop ends, we read each response and then remove the handle from the multi handle.
$status = curl_multi_remove_handle($mh, $ch); echo 'remove: ', curl_multi_strerror($status), "\n";
We print the result code as text to show that the removal succeeded.
Removing every handle before curl_multi_close leaves the easy
handles in a clean state.
$ php basic.php id 1 received remove: No error id 2 received remove: No error
Reusing a handle
An easy handle can belong to only one multi handle at a time, and it must be removed before it can be added again. This example runs three requests one after another with a single handle.
<?php
declare(strict_types=1);
$mh = curl_multi_init();
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
foreach (['first', 'second', 'third'] as $page) {
curl_setopt($ch, CURLOPT_URL, "https://httpbin.org/anything/$page");
curl_multi_add_handle($mh, $ch);
do {
curl_multi_exec($mh, $running);
curl_multi_select($mh);
} while ($running > 0);
$data = json_decode(curl_multi_getcontent($ch), true);
echo $data['url'], "\n";
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
For each page we set a new URL, add the handle to the multi handle, run the transfer, and remove the handle again.
curl_setopt($ch, CURLOPT_URL, "https://httpbin.org/anything/$page"); curl_multi_add_handle($mh, $ch);
Reusing the handle keeps its other options and lets cURL reuse the open
connection to the same host, which saves a new TCP and TLS handshake for
each request. Without the curl_multi_remove_handle call at the
end of the loop, the next curl_multi_add_handle call would
fail.
$ php reuse.php https://httpbin.org/anything/first https://httpbin.org/anything/second https://httpbin.org/anything/third
Cancelling a running request
Removing a handle whose transfer is still running aborts that transfer. This example starts a fast and a slow request and cancels the slow one as soon as the fast one is done.
<?php
declare(strict_types=1);
$mh = curl_multi_init();
$fast = curl_init('https://httpbin.org/delay/1');
$slow = curl_init('https://httpbin.org/delay/8');
foreach ([$fast, $slow] as $ch) {
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $ch);
}
$start = microtime(true);
do {
curl_multi_exec($mh, $running);
while ($info = curl_multi_info_read($mh)) {
if ($info['handle'] === $fast) {
echo "fast request finished\n";
curl_multi_remove_handle($mh, $fast);
echo "cancelling the slow request\n";
curl_multi_remove_handle($mh, $slow);
$running = 0;
}
}
if ($running > 0) {
curl_multi_select($mh);
}
} while ($running > 0);
curl_multi_close($mh);
$elapsed = microtime(true) - $start;
echo 'done in under 8 seconds: ', var_export($elapsed < 8, true), "\n";
echo 'slow response body: ', var_export(curl_multi_getcontent($slow), true), "\n";
The httpbin.org /delay/n endpoint responds after n
seconds. The slow request would take eight seconds.
if ($info['handle'] === $fast) {
...
curl_multi_remove_handle($mh, $slow);
$running = 0;
}
When curl_multi_info_read reports that the fast request has
finished, we remove both handles. Removing $slow closes its
connection, so the script does not wait for the server. We set
$running to zero to leave the loop, because no transfer is left
in the multi handle. This is how you implement patterns such as "use the first
mirror that answers".
The output confirms that the script finished in less than eight seconds and that the cancelled request has no body.
$ php cancel.php fast request finished cancelling the slow request done in under 8 seconds: true slow response body: ''
Limiting the number of parallel transfers
Adding hundreds of handles at once can overload the remote server or hit connection limits. Removing finished handles and adding new ones keeps a fixed number of transfers running.
<?php
declare(strict_types=1);
$queue = [];
foreach (range(1, 6) as $i) {
$queue[] = "https://httpbin.org/anything/item$i";
}
$maxActive = 2;
$active = 0;
$maxSeen = 0;
$results = [];
$mh = curl_multi_init();
$addNext = function () use (&$queue, &$active, $mh): void {
$url = array_shift($queue);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $ch);
$active++;
};
while ($queue && $active < $maxActive) {
$addNext();
}
do {
curl_multi_exec($mh, $running);
while ($info = curl_multi_info_read($mh)) {
$ch = $info['handle'];
$data = json_decode(curl_multi_getcontent($ch), true);
$results[] = basename($data['url']);
curl_multi_remove_handle($mh, $ch);
$active--;
if ($queue) {
$addNext();
}
}
$maxSeen = max($maxSeen, $active);
if ($active > 0) {
curl_multi_select($mh);
}
} while ($active > 0);
curl_multi_close($mh);
sort($results);
echo 'downloaded: ', implode(', ', $results), "\n";
echo "never more than $maxSeen transfers at once\n";
We have six URLs in a queue and allow at most two transfers at a time. The
$addNext closure takes the next URL from the queue and adds a
handle for it.
curl_multi_remove_handle($mh, $ch);
$active--;
if ($queue) {
$addNext();
}
Each time a transfer finishes, we remove its handle and immediately add the next one from the queue. This is called a rolling window: the number of running transfers stays at the limit until the queue is empty. We count the active handles ourselves and loop until none are left.
The responses can arrive in any order, so we sort the results before printing them.
$ php rolling.php downloaded: item1, item2, item3, item4, item5, item6 never more than 2 transfers at once
Return values
This example shows what the function returns in a few edge cases.
<?php
declare(strict_types=1);
$mh = curl_multi_init();
$ch = curl_init('https://httpbin.org/get');
$status = curl_multi_remove_handle($mh, $ch);
echo 'remove a handle that was never added: ', curl_multi_strerror($status), "\n";
curl_multi_add_handle($mh, $ch);
$status = curl_multi_add_handle($mh, $ch);
echo 'add the same handle twice: ', curl_multi_strerror($status), "\n";
$status = curl_multi_remove_handle($mh, $ch);
echo 'remove it: ', curl_multi_strerror($status), "\n";
$status = curl_multi_remove_handle($mh, $ch);
echo 'remove it again: ', curl_multi_strerror($status), "\n";
curl_multi_close($mh);
We try to remove a handle that was never added, add the same handle twice, and remove a handle twice.
Removing a handle that is not part of the multi handle is not treated as an
error: it returns CURLM_OK, so calling the function twice is
harmless. Adding a handle that already belongs to a multi handle fails with
CURLM_ADDED_ALREADY; this is the error you get when you forget to
remove a handle before reusing it.
$ php not_added.php remove a handle that was never added: No error add the same handle twice: The easy handle is already added to a multi handle remove it: No error remove it again: No error
Source
PHP curl_multi_remove_handle documentation
libcurl curl_multi_remove_handle documentation
In this article we have used the PHP curl_multi_remove_handle
function to clean up, reuse and cancel cURL transfers, and to limit the number
of parallel requests. See also the
curl_multi_exec,
curl_multi_getcontent and
curl_multi_close tutorials.
Author
List all PHP cURL tutorials.