Benchmarking PHP HTTP Clients

Published in PHP, HTTP on Nov 23, 2008

If you read my blog semi-regularly, you might remember when I mentioned that my book would be released later on this year. Unfortunately, that project had to be put on hold in favor of a few other projects. Now that those are winding down, however, I'm able to return to working on the book. I'm hoping the manuscript will be completed by the end of March 2009.

One of the interesting bits of research that I've done is benchmarking various mainstream PHP HTTP clients. Of course, we all know that there are lies, damned lies, statistics, and benchmarks, so take these with a grain of salt. They were run on my Sony Vaio, which is an Intel C2D T5550 @ 1.83GHz with 2 GB of RAM running Ubuntu Ibex and its standard php5 package. According to Speedtest.net, my Cox Cable connection has a 12,375 kb/s download rate and a 5,998 kb/s upload rate.

<?php
// pecl_http (1.6.1)
$response = http_get(
    'http://paste2.org/new-paste',
    array(
        'connecttimeout' =>  15
    )
);
echo 'http ', strlen($response), PHP_EOL;

// streams http wrapper
$response = file_get_contents('http://paste2.org/new-paste');
echo 'streams ', strlen($response), PHP_EOL;

// curl (php5-curl Ubuntu package:
// libcurl/7.18.2 OpenSSL/0.9.8g zlib/1.2.3.3 libidn/1.8)
$ch = curl_init('http://paste2.org/new-paste');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo 'curl ', strlen($response), PHP_EOL;

// PEAR::HTTP_Client (PEAR 1.7.2, HTTP_Client 1.2.1)
$error = error_reporting(E_ALL);
require_once 'HTTP/Client.php';
$client = new HTTP_Client();
$client->get('http://paste2.org/new-paste');
$response = $client->currentResponse();
$response = $response['body'];
echo 'pear ', strlen($response), PHP_EOL;
error_reporting($error);

// Zend_Http_Client (SVN r12780)
require_once 'Zend/Http/Client.php';
$client = new Zend_Http_Client('http://paste2.org/new-paste');
$response = $client->request()->getBody();
echo 'zend ', strlen($response), PHP_EOL;

The Ubuntu packages for Xdebug (php5-xdebug) and KCachegrind produced the following results for this script.

  • pecl_http - 20.08%
  • streams - 19.81%
  • curl - 19.83%
  • pear - 19.73%
  • zend - 19.88%

So the performance of these components is roughly equivalent. One thing that's interesting is that the call tree for PEAR is actually the longest (four calls underneath the one shown in the source here) and at the bottom is a call to gethostbyname(), which takes 18.97% of the script's runtime, putting the amount used by the calls above it at 0.76%. This suggests that the majority of the time taken by the other components is likely due to the same reason.

Let's try a slightly more complex request.

<?php
$post = array(
    'lang' => 'php',
    'description' => '',
    'code' => 'test',
    'parent' => '0'
);

// pecl_http
$response = http_post_fields(
    'http://paste2.org/new-paste',
    $post,
    null,
    array(
        'connecttimeout' => 15,
        'redirect' => 5,
    )
);
echo 'http ', strlen($response), PHP_EOL;

// streams http wrapper
$context = stream_context_create(array(
    'http' => array(
        'method' => 'POST',
        'header' => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query($post)
    )
));
$response = file_get_contents('http://paste2.org/new-paste', false, $context);
echo 'streams ', strlen($response), PHP_EOL;

// curl
$params = array(
    CURLOPT_URL => 'http://www.paste2.org/new-paste',
    CURLOPT_POST => true,
    CURLOPT_HEADER => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_POSTFIELDS => $post
);
$ch = curl_init();
foreach ($params as $key => $value) {
    curl_setopt($ch, $key, $value);
}
$response = curl_exec($ch);
curl_close($ch);
echo 'curl ', strlen($response), PHP_EOL;

// PEAR::HTTP_Client
$error = error_reporting(E_ALL);
require_once 'HTTP/Client.php';
$client = new HTTP_Client();
$client->post('http://paste2.org/new-paste', $post);
$response = $client->currentResponse();
$response = $response['body'];
echo 'pear ', strlen($response), PHP_EOL;
error_reporting($error);

// Zend_Http_Client
require_once 'Zend/Http/Client.php';
$client = new Zend_Http_Client('http://paste2.org/new-paste');
$client->setParameterPost($post);
$response = $client->request('POST')->getBody();
echo 'zend ', strlen($response), PHP_EOL;

And here are the Xdebug + KCachegrind results for the execution of this script.

  • pecl_http - 12.56%
  • streams - 25.02%
  • curl - 12.69%
  • pear - 24.81%
  • zend - 24.81%

The gethostbyname() call in the PEAR call stack again takes up the majority of its runtime, 21.05% in this case. That puts the remainder of the time for PEAR at 3.76%. pecl_http and curl are roughly equivalent in performance to each other and twice that of the others. Oddly, streams (a C extension like pecl_http and curl) suffers a performance difference similar to the libraries written in PHP.

I have a semi-educated guess as to why this is. PEAR makes two gethostbyname() calls to process the request, presumably one for the initial POST and one for a GET that follows because the POST response includes a Location header. Zend appears to make two stream_socket_client() calls for the same reason. Streams do not appear to implicitly cache DNS lookups, so the HTTP streams wrapper is most likely in the same situation.

The existence of the CURLOPT_DNS_USE_GLOBAL_CACHE option and the http.request.datashare.dns configuration setting and the fact that both are enabled by default lead me to believe that the curl and pecl_http extensions do cache DNS lookups and thus don't suffer the performance hit of repeating them.

Update: As far as I can tell, Ubuntu does not have a DNS cache running by default. nscd is available as a package for it and I tried installing it, but found this comment in the configuration file:

# hosts caching is broken with gethostby* calls, hence is now disabled
# per default. See /usr/share/doc/nscd/NEWS.Debian.

So I uninstalled that and followed this tutorial to install, configure, and test dnsmasq instead:

http://ubuntu.wordpress.com/2006/08/02/local-dns-cache-for-faster-browsing/

I also amended a few oversights in my original example. The cURL options array should include CURLOPT_FOLLOWLOCATION and the pecl_http options array should include redirect. Using a GET request that results in a redirect as the test subject, I got the following results.

Before installing dnsmasq:

  • pecl_http - 12.75%
  • streams - 25.27%
  • curl - 12.92%
  • pear - 32.82%
  • zend - 24.50%

After installing dnsmasq:

  • pecl_http - 19.58%
  • streams - 21.40%
  • curl - 17.30%
  • pear - 28.20%
  • zend - 21.10%

So, a local OS-level DNS cache does bring the performance of these clients significantly closer to equal footing.