login: get_codex_user_agent() re-runs os_info::get() per HTTP client build, spawning lsb_release/dpkg-query/getconf (~27 ms each on Linux)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- rust
- Domain
- backend, performance
Research direction
Start in codex-rs/login/src/auth/default_client.rs at get_codex_user_agent() and default_headers(), then inspect the existing cached statics in that module. Verify that the OS probe is performed once per process while USER_AGENT_SUFFIX is still reflected per call, and confirm the repeated client-build paths no longer spawn the listed Linux subprocesses.
Written by the indexing model from the issue text.
Description
Summary
get_codex_user_agent() (codex-rs/login/src/auth/default_client.rs) calls os_info::get() on every invocation, and it is called from default_headers(), which runs on every default HTTP client build:
pub fn get_codex_user_agent() -> String {
let build_version = env!("CARGO_PKG_VERSION");
let os_info = os_info::get(); // <- spawns subprocesses on Linux, every call
...
}
pub fn default_headers() -> HeaderMap {
...
if let Ok(user_agent) = HeaderValue::from_str(&get_codex_user_agent()) {
On Linux, os_info::get() shells out. Verified with strace -f -e trace=execve against os_info 3.14.0 (the version in codex-rs/Cargo.lock):
execve("/usr/bin/lsb_release", ["lsb_release", "-a"], ...) = 0
execve("/usr/bin/dpkg-query", ["dpkg-query", "-f", "${Version} ${Provides}\n", "-W", "lsb-core", ...]) = 0
execve("/usr/bin/getconf", ["getconf", "LONG_BIT"], ...) = 0
lsb_release is a Python script on Debian/Ubuntu (#!/usr/bin/python3 -Es), so each call pays a Python interpreter startup plus a dpkg-query package scan. There were also ~45 failed execve attempts walking PATH before lsb_release resolved.
Measured cost on Ubuntu 24.04 x86-64 (release build, 50 iterations, warm cache):
os_info::get(): ~27 ms per call
The result is immutable for the process lifetime, and sibling values in the same module (ORIGINATOR, REQUIREMENTS_RESIDENCY, USER_AGENT_SUFFIX) are already cached in statics — the OS probe is the one that is not.
Why this matters
default_headers() is on paths that run repeatedly, not once at startup:
default_http_client_builder()→create_client()/create_client_for_route(), so every default client build re-probes the OScodex-rs/chatgpt/src/chatgpt_client.rscallscreate_client()per requestcodex-rs/analytics/src/client.rscallscreate_client()per analytics uploadcodex-rs/core/src/client.rs::build_api_transportbuilds a client per compaction / realtime / memories call, andconnect_websocketpassesdefault_headers()on every websocket connect (prewarm and every reconnect)codex-rs/core-plugins/src/{remote.rs,startup_sync.rs}andcodex-rs/tui/src/updates.rsalso call it per request
So a workload with many short-lived threads/turns spawns 3 subprocesses and burns ~27 ms of wall time (mostly CPU across the process tree) per client build, purely to re-derive a constant string. This is closely related to #29369 (fresh reqwest::Client per request) — the same call site is hot for both reasons — but caching the client and caching the OS probe are independent fixes, and the OS probe is the more expensive one on Linux.
Secondary concern: spawning lsb_release/dpkg-query per request is noisy in sandboxed and audited environments (process-exec auditing, seccomp/sandbox policies, container images that intentionally omit lsb_release), and it is a per-request dependency on the host having those binaries.
Production impact (downstream harness)
We run codex app-server headless on Linux x86-64 as an agent fleet (many short-lived threads/turns per process). On tag rust-v0.146.0, strace showed lsb_release → dpkg-query spawns on the per-turn client-build path, and caching the user-agent in a static LazyLock<String> was one of two patches (the other being a shared TLS root store, filed separately) that took us from 0.36–0.39 CPU-seconds per turn to 0.15 CPU-s/turn on real authenticated turns. Those numbers cover both patches plus disabling unused features, so treat them as the aggregate; the isolated cost of this path is the ~27 ms + 3 subprocesses per call measured above.
Proposed fix
Cache the whole user-agent (or at minimum the OS fragment) in a process-level static, matching how ORIGINATOR is already handled:
static USER_AGENT_OS_FRAGMENT: LazyLock<String> = LazyLock::new(|| {
let info = os_info::get();
format!("{} {}; {}", info.os_type(), info.version(), info.architecture().unwrap_or("unknown"))
});
The mutable USER_AGENT_SUFFIX still needs to be read per call, so caching the OS fragment (rather than the full string) keeps current behavior exactly while removing the subprocess spawns. Happy to send a PR.
Environment
- Repo state inspected:
main@0042b00986b9cc73c82c93f94e93d747818228be os_info3.14.0 (percodex-rs/Cargo.lock), measured standalone at that version- Linux x86-64 (Ubuntu 24.04), headless
codex app-servervia JSON-RPC (no TUI)
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.5k
- Avg merge
- 1m
- Merged PRs (30d)
- 1k
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openai/codex
-
enhancement remote
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
bug CLI windows-os
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
macOS sandbox blocks hw.optional.arm64 sysctl, causing Flutter to misdetect Apple Silicon as x64 Openbug CLI sandbox
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug CLI TUI
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
CLI config enhancement skills
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
gitbutlerapp/gitbutler#15998 · 1 comment ·
-
bug triage:deciding
Difficulty 1/5 Under an hour Newbie friendliness 88/100
open-telemetry/otel-arrow#4132 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bitcoindevkit/bdk-ffi#1125 ·