ZetCode

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.

basic.php
<?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.

reuse.php
<?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.

cancel.php
<?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.

rolling.php
<?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.

not_added.php
<?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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all PHP cURL tutorials.