#12987·composer

Performance: Composer attempts HTTP/3, even against servers that don't support it, including repo.packagist.org

Author: nsfisisCreated Jul 18, 2026Updated Aug 25, 2026

Summary

Composer attempts HTTP/3 if libcurl supports it, without checking whether the destination server supports it. Because repo.packagist.org does not support HTTP/3, Composer's requests to the server always fall back to HTTP/2, which adds unnecessary latency to many requests.

Versions

  • Composer: 2.10.2 (2026-07-01 11:24:45)
  • PHP: 8.5.8
  • curl: 8.21.0, OpenSSL/3.6.3, ngtcp2/1.23.0, nghttp3/1.16.0
  • OS: Ubuntu 24.04, x86_64

Problem

In src/Composer/Util/Http/CurlDownloader.php,

php
// initDownload()
if (!$willUseProxy && \defined('CURL_VERSION_HTTP3') && \defined('CURL_HTTP_VERSION_3') && (CURL_VERSION_HTTP3 & $features) !== 0) {
    curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3);
} elseif (...) {
    curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
}

This only checks whether the libcurl can speak HTTP/3, and never checks whether the destination server supports it.

repo.packagist.org does not support HTTP/3:

$ curl -sv --http3-only -o /dev/null https://repo.packagist.org/packages.json
* Host repo.packagist.org:443 was resolved.
...
* ngtcp2_conn_handle_expiry returned error: ERR_HANDSHAKE_TIMEOUT
* ngtcp2_conn_handle_expiry returned error: ERR_HANDSHAKE_TIMEOUT
* Failed to connect to repo.packagist.org:443 after 10202 ms: Failed sending data to the peer
* closing connection #0

In Composer, curl will fall back to HTTP/2 if the server does not respond, but the protocol switch cost is not free:

bash
$ hyperfine \
    --warmup 2 \
    --runs 15 \
    -n "curl-http2" 'curl -s -o /dev/null --http2 https://repo.packagist.org/packages.json' \
    -n "curl-http3" 'curl -s -o /dev/null --http3 https://repo.packagist.org/packages.json'

Benchmark 1: curl-http2
  Time (mean ± σ):      85.0 ms ±  30.2 ms    [User: 13.9 ms, System: 8.6 ms]
  Range (min … max):    53.8 ms … 159.3 ms    15 runs

Benchmark 2: curl-http3
  Time (mean ± σ):     379.7 ms ± 105.1 ms    [User: 14.9 ms, System: 9.9 ms]
  Range (min … max):   265.2 ms … 628.6 ms    15 runs

Summary
  curl-http2 ran
    4.47 ± 2.01 times faster than curl-http3

As a quick experiment, I removed the HTTP/3 branch entirely (always use HTTP/2 in my environment) and benchmarked.

Command:

bash
$ hyperfine \
    --warmup 1 \
    --runs 15 \
    --prepare "bash enable-http3.sh" \
    --prepare "bash disable-http3.sh" \
    --ignore-failure \
    -n "composer-2.10.2-http3" "php composer/bin/composer require --no-install --no-audit --no-interaction --working-dir=proj-http3 composer/composer" \
    -n "composer-2.10.2-http2" "php composer/bin/composer require --no-install --no-audit --no-interaction --working-dir=proj-http2 composer/composer"

enable-http3.sh / disable-http3.sh switches HTTP/3 support in CurlDownloader.php.

diff
-            if (!$willUseProxy && \defined('CURL_VERSION_HTTP3') && \defined('CURL_HTTP_VERSION_3') && (CURL_VERSION_HTTP3 & $features) !== 0) {
+            if (false) {

Result:

Benchmark 1: composer-2.10.2-http3
  Time (mean ± σ):      1.270 s ±  0.222 s    [User: 0.333 s, System: 0.095 s]
  Range (min … max):    1.048 s …  1.771 s    15 runs

Benchmark 2: composer-2.10.2-http2
  Time (mean ± σ):     825.7 ms ± 124.2 ms    [User: 339.0 ms, System: 91.2 ms]
  Range (min … max):   640.4 ms … 1070.3 ms    15 runs

Summary
  composer-2.10.2-http2 ran
    1.54 ± 0.35 times faster than composer-2.10.2-http3

The fallback cost from HTTP/3 to HTTP/2 is measurable, real overhead.

Attempting HTTP/3 and falling back to HTTP/2 is not a problem by itself, but given that the de-facto standard repository, repo.packagist.org, does not support it, it may be worth asking whether HTTP/3 should be enabled unconditionally by default.