http: keep connections alive across navigations

CURLMOPT_MAXCONNECTS was never set, so libcurl used its default of 4x the
number of easy handles currently attached to the multi. Handles are added
and removed per transfer, so between page loads that default collapses to
roughly zero and every cached connection is evicted: revisiting a host
after browsing elsewhere re-paid connect + TLS every time.

Measured over 25 navigations across 5 sites (5 rounds, ReleaseFast), median
warm navigation drops from 0.73s to 0.42s on news.ycombinator.com and from
0.71s to 0.42s on github.com; total navigation time 11.1s -> 7.3s. Loading
one host repeatedly was already fast and is unchanged; the win is on
cross-site browsing, which is what agents and crawlers actually do.
This commit is contained in:
Adrià Arrufat authored and Karl Seguin committed 2026-09-02 11:00:04 +08:00
1 parent 2745d8510a
commit 39bd76e079
2 files changed
+5 -1

No files matched your search

+3
View File
@@ -668,6 +668,9 @@ pub const Handles = struct {
errdefer libcurl.curl_multi_cleanup(multi) catch {};
try libcurl.curl_multi_setopt(multi, .max_host_connections, config.httpMaxHostOpen());
// Default is 4x the attached easy handles, i.e. ~0 between page loads,
// so keepalive connections were evicted on every cross-site navigation.
try libcurl.curl_multi_setopt(multi, .max_connects, 128);
return .{ .multi = multi };
}
+2 -1
View File
@@ -239,6 +239,7 @@ pub const CurlHttpVersion = enum(c_long) {
pub const CurlMOption = enum(c.CURLMoption) {
max_host_connections = c.CURLMOPT_MAX_HOST_CONNECTIONS,
max_connects = c.CURLMOPT_MAXCONNECTS,
};
pub const CurlInfo = enum(c.CURLINFO) {
@@ -742,7 +743,7 @@ pub fn curl_multi_cleanup(multi: *CurlM) ErrorMulti!void {
pub fn curl_multi_setopt(multi: *CurlM, comptime option: CurlMOption, value: anytype) ErrorMulti!void {
const opt: c.CURLMoption = @intFromEnum(option);
const code = switch (option) {
.max_host_connections => blk: {
.max_host_connections, .max_connects => blk: {
const n: c_long = switch (@typeInfo(@TypeOf(value))) {
.comptime_int, .int => @intCast(value),
else => @compileError("expected integer for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),