ZetCode

PHP curl_multi_getcontent Function

last modified November 14, 2026

The PHP curl_multi_getcontent function returns the response body of a cURL handle that was run as part of a multi handle. In this article we show how to read the results of parallel requests, how to check whether a transfer succeeded, and how to process responses as soon as they arrive.

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_getcontent(CurlHandle $handle): ?string

When we run a single request with curl_exec, the function returns the body directly. With the multi interface, curl_multi_exec only drives the transfers; it does not return any data. The bodies stay stored in the individual easy handles, and curl_multi_getcontent retrieves them.

The function returns the body only when the handle has the CURLOPT_RETURNTRANSFER option set. Otherwise it returns null. A transfer that failed returns an empty string.

Reading parallel responses

The first example downloads two resources in parallel and prints their bodies.

basic.php
<?php

declare(strict_types=1);

$urls = [
    'https://httpbin.org/robots.txt',
    'https://httpbin.org/user-agent',
];

$mh = curl_multi_init();
$handles = [];

foreach ($urls as $url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_USERAGENT, 'zetcode-demo/1.0');
    curl_multi_add_handle($mh, $ch);
    $handles[$url] = $ch;
}

do {
    curl_multi_exec($mh, $running);
    curl_multi_select($mh);
} while ($running > 0);

foreach ($handles as $url => $ch) {
    echo "== $url\n";
    echo curl_multi_getcontent($ch), "\n";
    curl_multi_remove_handle($mh, $ch);
}

curl_multi_close($mh);

We create one easy handle per URL and add each one to the multi handle. We keep the handles in an array keyed by URL, because we need them again after the transfers finish.

do {
    curl_multi_exec($mh, $running);
    curl_multi_select($mh);
} while ($running > 0);

This is the standard loop that runs all transfers. The curl_multi_exec function does as much work as it can without blocking and stores the number of unfinished transfers in $running. The curl_multi_select function waits until there is activity on one of the connections, so the loop does not spin at full CPU.

echo curl_multi_getcontent($ch), "\n";
curl_multi_remove_handle($mh, $ch);

Once the loop ends, every body is ready. We read each one with curl_multi_getcontent and then detach the handle from the multi handle with curl_multi_remove_handle.

$ php basic.php
== https://httpbin.org/robots.txt
User-agent: *
Disallow: /deny

== https://httpbin.org/user-agent
{
  "user-agent": "zetcode-demo/1.0"
}

The CURLOPT_RETURNTRANSFER option

The CURLOPT_RETURNTRANSFER option decides whether cURL keeps the response body or writes it straight to the output. This example leaves the option out.

no_returntransfer.php
<?php

declare(strict_types=1);

$mh = curl_multi_init();

$ch = curl_init('https://httpbin.org/robots.txt');
curl_multi_add_handle($mh, $ch);

do {
    curl_multi_exec($mh, $running);
    curl_multi_select($mh);
} while ($running > 0);

$content = curl_multi_getcontent($ch);
echo 'curl_multi_getcontent returned: ';
var_dump($content);

curl_multi_remove_handle($mh, $ch);
curl_multi_close($mh);

Without the option, cURL prints the body to the standard output as soon as it arrives, and nothing is stored in the handle. That is why the body appears in the output before our own message, and why curl_multi_getcontent returns null. Always set CURLOPT_RETURNTRANSFER to true on handles whose content you want to read.

$ php no_returntransfer.php
User-agent: *
Disallow: /deny
curl_multi_getcontent returned: NULL

Decoding JSON responses

APIs usually return JSON. The content returned by curl_multi_getcontent is a plain string, so we decode it ourselves.

json_responses.php
<?php

declare(strict_types=1);

$mh = curl_multi_init();
$handles = [];

foreach (['php', 'python', 'rust'] as $lang) {
    $ch = curl_init('https://httpbin.org/get?lang=' . $lang);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_multi_add_handle($mh, $ch);
    $handles[$lang] = $ch;
}

do {
    curl_multi_exec($mh, $running);
    curl_multi_select($mh);
} while ($running > 0);

foreach ($handles as $lang => $ch) {
    $data = json_decode(curl_multi_getcontent($ch), true, flags: JSON_THROW_ON_ERROR);
    printf("%-6s -> %s\n", $lang, $data['url']);
    curl_multi_remove_handle($mh, $ch);
}

curl_multi_close($mh);

We send three requests with different query strings. The httpbin.org /get endpoint echoes the request back as JSON, including the full URL.

$data = json_decode(curl_multi_getcontent($ch), true, flags: JSON_THROW_ON_ERROR);

The JSON_THROW_ON_ERROR flag makes json_decode throw a JsonException on invalid input instead of silently returning null. The results are printed in the order of the $handles array, not in the order the responses arrived.

$ php json_responses.php
php    -> https://httpbin.org/get?lang=php
python -> https://httpbin.org/get?lang=python
rust   -> https://httpbin.org/get?lang=rust

Checking the result of each transfer

The content alone does not tell us whether a request succeeded: an HTTP error and a network failure can both produce an empty body. This example checks each transfer properly.

status_check.php
<?php

declare(strict_types=1);

$urls = [
    'https://httpbin.org/status/200',
    'https://httpbin.org/status/404',
    'https://httpbin.org/html',
    'https://nonexistent.invalid/',
];

$mh = curl_multi_init();

foreach ($urls as $url) {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_multi_add_handle($mh, $ch);
}

$results = [];

do {
    curl_multi_exec($mh, $running);

    while ($info = curl_multi_info_read($mh)) {
        $ch = $info['handle'];
        $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);

        if ($info['result'] !== CURLE_OK) {
            $results[$url] = 'failed: ' . curl_strerror($info['result']);
        } else {
            $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $body = curl_multi_getcontent($ch);
            $results[$url] = "HTTP $code, body length " . strlen($body);
        }

        curl_multi_remove_handle($mh, $ch);
    }

    if ($running > 0) {
        curl_multi_select($mh);
    }
} while ($running > 0);

curl_multi_close($mh);

foreach ($urls as $url) {
    echo "$url: {$results[$url]}\n";
}

The curl_multi_info_read function returns a message for each finished transfer. Its result element holds the cURL result code of that transfer.

if ($info['result'] !== CURLE_OK) {
    $results[$url] = 'failed: ' . curl_strerror($info['result']);
}

In the multi interface this result code is the reliable way to detect a network failure. The curl_error and curl_errno functions are meant for curl_exec; for a handle run by a multi handle they report no error even when the transfer failed. The curl_strerror function turns the code into a readable message.

$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$body = curl_multi_getcontent($ch);

A transfer that completed can still carry an HTTP error status, so we read the status code with curl_getinfo. The output shows that both the 200 and the 404 status endpoints return an empty body; only the status code tells them apart.

$ php status_check.php
https://httpbin.org/status/200: HTTP 200, body length 0
https://httpbin.org/status/404: HTTP 404, body length 0
https://httpbin.org/html: HTTP 200, body length 3741
https://nonexistent.invalid/: failed: Could not resolve hostname

Processing responses as they complete

Waiting for all transfers before reading any of them is simple, but the slowest request then delays everything. This example reads each response as soon as its transfer finishes.

as_completed.php
<?php

declare(strict_types=1);

$mh = curl_multi_init();

foreach ([3, 1, 2] as $delay) {
    $ch = curl_init("https://httpbin.org/delay/$delay");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_PRIVATE, "delay $delay s");
    curl_multi_add_handle($mh, $ch);
}

do {
    curl_multi_exec($mh, $running);

    while ($info = curl_multi_info_read($mh)) {
        $ch = $info['handle'];
        $label = curl_getinfo($ch, CURLINFO_PRIVATE);
        $data = json_decode(curl_multi_getcontent($ch), true);

        echo "finished: $label, url {$data['url']}\n";
        curl_multi_remove_handle($mh, $ch);
    }

    if ($running > 0) {
        curl_multi_select($mh);
    }
} while ($running > 0);

curl_multi_close($mh);

The httpbin.org /delay/n endpoint waits n seconds before it responds. We start the requests in the order 3, 1, 2.

curl_setopt($ch, CURLOPT_PRIVATE, "delay $delay s");

The CURLOPT_PRIVATE option attaches our own label to the handle. We read it back with curl_getinfo($ch, CURLINFO_PRIVATE) when the transfer finishes, so we don't need a separate array that maps handles to requests.

while ($info = curl_multi_info_read($mh)) {
    $ch = $info['handle'];
    ...
    curl_multi_remove_handle($mh, $ch);
}

After every call to curl_multi_exec we drain the queue of finished transfers. Each response is decoded and printed right away, and its handle is removed from the multi handle. The responses arrive in the order of their delays, not in the order the requests were started.

$ php as_completed.php
finished: delay 1 s, url https://httpbin.org/delay/1
finished: delay 2 s, url https://httpbin.org/delay/2
finished: delay 3 s, url https://httpbin.org/delay/3

Source

PHP curl_multi_getcontent documentation

PHP curl_multi_info_read documentation

In this article we have used the PHP curl_multi_getcontent function to read the responses of parallel cURL requests. See also the curl_multi_exec, curl_multi_remove_handle and curl_getinfo 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.