perf(codex): pace WebSocket connects adaptively instead of at a fixed 1s - #135
Open
momomuchu wants to merge 2 commits into
Open
perf(codex): pace WebSocket connects adaptively instead of at a fixed 1s#135momomuchu wants to merge 2 commits into
momomuchu wants to merge 2 commits into
Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Spacing fresh WebSocket starts (529e1d6) protects an origin from upgrade rejection bursts, but it charges for that protection continuously. Continuation is off by default, so every request opens a fresh socket and pays the spacing. A single process is therefore capped near one generation per second however healthy the origin is. Measured on an idle account, 12 trivial requests through one process: 13.27s (0.90 req/s). The gate is process-global, so the ceiling did not move with any downstream concurrency setting. Spacing now starts at the configured floor (zero by default), widens to 1s on a rejected upgrade and doubles up to 8s, then halves back toward the floor after 20 consecutive successful connects. The original protection still engages, but only once an origin has actually refused something. Operators who want guaranteed pacing can set a floor the proxy never relaxes below, via CCP_CODEX_WS_CONNECT_SPACING_MS or codex.websocketConnectSpacingMs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017NMJJGbVCSRiY2aGAryeUv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Spacing fresh WebSocket connection starts (529e1d6, "limit codex websocket connection churn") protects an origin from upgrade-rejection bursts. The protection is right, but it is billed continuously rather than on demand.
codex_previous_response_id()defaults tofalse, so continuation is off and there is no pool owner for an ordinary request. Every generation therefore opens a fresh socket and pays the spacing.WS_CONNECT_GATEis a process-globalLazy<WebSocketConnectGate>whosewait_to_startsleeps toprevious + 1swhile holding the mutex, so the spacing is not per-connection — it serializes the whole process.The result is a hard ceiling of roughly one generation per second per process, independent of how healthy the origin is and of any concurrency the caller configures.
Measured
One process, idle account, 12 trivial
/v1/messagesrequests, all issued concurrently:Throughput tracked the number of processes, not the number of in-flight requests — the signature of a process-global serializer.
Change
Spacing becomes the price of an observed rejection instead of a standing tax:
Duration::ZEROby default),connect_with_policynow reports each outcome to the gate (note_origin_forbidden/note_connect_success). The existing single 403 retry-after-cooldown is unchanged, and the pre-existingconnect_gate_*locks still pass because a gate constructed with an explicit spacing keeps that spacing as its floor.Both signal paths use
try_lockand return on contention: losing a widen signal is harmless (the next rejection carries it) whereas blocking there would stall the connect path.Operators who want guaranteed pacing set a floor the gate never relaxes below, via
CCP_CODEX_WS_CONNECT_SPACING_MSorcodex.websocketConnectSpacingMs.Result
Controlled A/B, same account, same window, 30 concurrent requests against one process:
5.2x on a single process, with fewer failures rather than more.
A 12-request burst shows no difference between the two — at that size the per-request latency masks the gate. The regression only becomes visible once the burst exceeds the latency-bandwidth product, which is worth knowing for anyone re-measuring this.
Deployed locally across 12 processes afterwards: bursts of 10/30/60 concurrent requests completed 176/176 with no errors, and throughput kept rising with concurrency (no saturation up to 60), where previously it was pinned near 1 req/s.
Tests
New unit locks in
src/providers/codex/websocket.rs:idle_gate_starts_connections_without_spacing_themgate_spaces_starts_after_the_origin_rejects_an_upgraderepeated_rejections_widen_the_spacing_up_to_a_capsustained_success_relaxes_the_spacing_back_to_zeroa_configured_floor_is_never_relaxed_awayThe last two are the negative controls: without relaxation a single 403 would tax the process for its whole life, and without a respected floor an operator's explicit pacing would be optimised away.
just check-cigreen on the commit:✓ format-check ✓ clippy ✓ build ✓ test.Scope
Codex WebSocket connect pacing only. No change to continuation, pooling, model routing, request translation, other providers, or the 403 retry contract.