Performance: Composer attempts HTTP/3, even against servers that don't support it, including repo.packagist.org
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,
// 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 #0In Composer, curl will fall back to HTTP/2 if the server does not respond, but the protocol switch cost is not free:
$ 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-http3As a quick experiment, I removed the HTTP/3 branch entirely (always use HTTP/2 in my environment) and benchmarked.
Command:
$ 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.
- 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-http3The 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.
Source: composer/composer