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

Open 💬 1 comment Opened Jun 21, 2026 by HamburgJ

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 via reqwest::Client::builder()...build() each call, with no LazyLock/OnceLock memoization of the client (sibling globals ORIGINATOR / REQUIREMENTS_RESIDENCY / USER_AGENT_SUFFIX are cached this way; the client is not). It applies build_reqwest_client_with_custom_ca(...), whose CA path (codex-rs/codex-client/src/custom_ca.rs) does an fs::read of CODEX_CA_CERTIFICATE / SSL_CERT_FILE per build.
  • codex-rs/core/src/client.rs constructs ReqwestTransport::new(build_reqwest_client()) at four sites: compaction (~L502), realtime call (~L602), memories (~L633), and inside stream_responses_api (~L1288). ModelClientState caches a websocket session but holds no cached reqwest::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-invokes stream(...), which on the HTTP path re-enters stream_responses_api and 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗