Keepalive failures repeatedly rescan the connection pool
perfloop/curl · INEFFICIENT ALGORITHM
https://perfloop.ai/t/oss/case_v3hzkb2sqf
Verdict
VERIFIED · settled 2026-08-17
What happened: The paired measurements met the required improvement.
Hypothesis
I ran a source-trace check from `curl_easy_upkeep` through `Curl_cpool_upkeep`, `cpool_foreach`, and `conn_upkeep`. It showed that `cpool_foreach` stops when its callback returns 1, while `Curl_cpool_upkeep` immediately invokes a fresh traversal. In `conn_upkeep`, a due keepalive error on a connection that is not in use calls `cpool_conn_close` and returns 1.
Each fresh traversal starts its hash iterator again and rechecks connections from the beginning. If H retained connections precede F due, failed, idle connections in iterator order, each of those F closes revisits the H entries, including their timestamp checks; that gives the traversal an O(H×F + N) shape and can become quadratic when H and F grow together. Upkeep is explicitly scheduled by the application rather than by a background timer, and the default upkeep interval is 60 seconds. A multi handle's documented default cache grows to four times its added easy handles, so a large mixed error burst is the relevant trigger. The runtime cost is unknown because this source check did not profile it.
The removed delta is the repeated whole-pool rescan and timestamp checking after each failed close. A case session should arrange retained entries before injected keepalive failures, make them due, and call `curl_easy_upkeep` once. It should sweep N and the H/F split, count callback and clock-check executions, and measure upkeep CPU and elapsed time before and after a deletion-safe traversal. The proof must show linear callback growth after the change while still closing every failed idle connection.
Change to test: Make upkeep close failed idle connections without restarting the pool traversal from its first hash bucket after every removal. Use a deletion-safe iterator or defer selected removals until one scan completes, while preserving the existing close decision for each connection.
Where it lives
perfloop/curl · lib/easy.c
Evidence
documented default 60-second interval: synthetic overdue retained/failed sweep through 512 connections · 10 sample pairs
| metric | baseline | candidate | paired median change | confidence range | required | result |
|---|---|---|---|---|---|---|
upkeep_elapsed_us |
440179 |
29009 |
−93.4% (−411170) |
−426138 to −406233 |
< −22009 |
PASSED |
Checks: 2 of 2 passed. Verification: no defect found.