core: build_reqwest_client() allocates a fresh reqwest::Client (new connection pool) per request — no keep-alive reuse on the HTTP path, including across retries
Summary
build_reqwest_client() constructs a brand-new reqwest::Client on every call, and it is called per outgoing request on the HTTP ReqwestTransport path. Because each reqwest::Client owns its own connection pool, no TCP/TLS keep-alive connection is reused across requests — or across retries of the same request, since a retryable error re-enters the HTTP request path and rebuilds the client. Every attempt pays a fresh handshake. When a custom CA is configured (CODEX_CA_CERTIFICATE / SSL_CERT_FILE), each build also re-reads and re-parses the CA PEM from disk.
Root cause (current main)
codex-rs/login/src/auth/default_client.rs:build_reqwest_client()->try_build_reqwest_client()builds a new client viareqwest::Client::builder()...build()each call, with noLazyLock/OnceLockmemoization of the client (sibling globalsORIGINATOR/REQUIREMENTS_RESIDENCY/USER_AGENT_SUFFIXare cached this way; the client is not). It appliesbuild_reqwest_client_with_custom_ca(...), whose CA path (codex-rs/codex-client/src/custom_ca.rs) does anfs::readofCODEX_CA_CERTIFICATE/SSL_CERT_FILEper build.codex-rs/core/src/client.rsconstructsReqwestTransport::new(build_reqwest_client())at four sites: compaction (~L502), realtime call (~L602), memories (~L633), and insidestream_responses_api(~L1288).ModelClientStatecaches a websocket session but holds no cachedreqwest::Client, so every HTTP-path call rebuilds.- Retries: on a retryable network error the turn-level retry loop (
stream_max_retries,codex-rs/core/src/session/turn.rs) re-invokesstream(...), which on the HTTP path re-entersstream_responses_apiand rebuilds the client — so retries of a request can never reuse the connection they just made.
Impact
Extra TCP+TLS handshakes on the HTTP ReqwestTransport path: providers that do not use the prewarmed websocket (OSS / Ollama / custom base_url), the compaction/memory/file calls, and retries. With a custom enterprise CA set, every request additionally re-reads + re-parses the CA PEM from disk. The first-party OpenAI streaming path is largely shielded because it prefers the prewarmed/reused websocket transport (ModelClientSession), so this is a secondary — not dominant — latency cost, but it is pure waste on the affected paths.
Proposed fix
Memoize the default client so the connection pool (and parsed CA config) is reused. reqwest::Client is internally Arc and cheap to clone, so a process-level LazyLock<reqwest::Client> (or a small OnceLock keyed by the few variants: sandboxed/no_proxy and custom-CA fingerprint) lets callers clone a shared instance instead of rebuilding. try_build_reqwest_client() stays available for callers that need structured CA-failure handling. This adds no public config surface.
Happy to put up the PR if useful.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗