Repository navigation
WebSocket fallback should recover after transient transport failures #23665
Description
What problem are you seeing?
When supports_websockets = true is configured for a Responses provider, Codex correctly starts by using the WebSocket transport. However, if a WebSocket streaming request disconnects and exhausts the configured stream_max_retries, the session falls back to HTTPS/SSE and appears to keep WebSockets disabled for the rest of that Codex session.
This is helpful for environments where WebSockets are genuinely unsupported, such as some proxies, but it also means a transient network/provider interruption can leave an otherwise healthy session on HTTPS/SSE until Codex is restarted.
Why this matters
For temporary failures, the service or network may recover shortly after fallback. In that case, users would expect Codex to eventually try WebSockets again without needing to restart the app/session.
At the same time, the recovery behavior should not make permanently broken proxy environments worse by repeatedly trying WebSockets forever.
Suggested behavior
Keep the existing supports_websockets configuration and avoid adding a new user-facing setting.
A possible approach:
- after WebSocket fallback activates, disable WebSockets only for a cooldown window instead of permanently for the session
- allow a small bounded number of recovery attempts per session
- after the recovery budget is exhausted, leave the session on HTTPS/SSE
- reset the recovery budget only after a WebSocket stream reaches
response.completed, not just after the WebSocket connection opens
This would let transient failures recover automatically while preserving the current fallback behavior for proxies or environments that consistently fail WebSocket transport.
Prototype branch
I prepared a small prototype branch here:
https://github.com/qindongliang/codex/tree/codex/websocket-fallback-recovery
Commit:
067853cb79 Recover websocket transport after fallback cooldown
The branch uses cooldown-based recovery and resets the recovery budget after a successful WebSocket completion. I could not open a PR because this repository currently limits pull request creation to collaborators.
Local testing
From the prototype branch:
just fmtgit diff --checkRUSTC="$HOME/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/rustc" "$HOME/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/cargo" test -p codex-core websocket_PATH="$HOME/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin:$PATH" RUSTC="$HOME/.rustup/toolchains/1.93.0-aarch64-apple-darwin/bin/rustc" just fix -p codex-core
The targeted WebSocket test run passed: 6 unit tests and 48 filtered integration tests.
Activity
Tonyfenwick1982 commentedon Sep 5, 2026
Follow-up with measured bandwidth impact on Windows, and a request for bounded, payload-aware WebSocket recovery. The affected user explicitly authorized this diagnostic report.
Environment: Windows 11 x64, Codex desktop 26.901.5003.0, selected app-server codex-cli 0.153.3, ChatGPT authentication, an explicit local HTTP proxy. HTTP_PROXY and HTTPS_PROXY were set, and the desktop was fully restarted.
The explicit proxy workaround reported in #29958 successfully restored Responses WebSocket connections. Several subsequent coding tasks established successful WebSockets over multiple hours. It did not eliminate later image-heavy send failures.
Sanitized observation on 2026-09-05 (UTC):
Request-body preparation does not prove full delivery, and rotating logs are incomplete. The proxy also logged upstream resets, but its available entries lack timestamps; they do not establish which component caused the WebSocket resets. No decryption or large-payload A/B replay was performed.
Requested recovery behavior:
The user-facing goal is that an ordinary network interruption recovers automatically and an expensive repeated-upload loop stops with one actionable explanation. Users should not have to inspect transport logs, manually restart the app, and rebuild tasks repeatedly.
Only aggregate operational measurements and public issue references are shared here. No raw sessions, prompts, project names, local paths, image contents, account identifiers, credentials, or private endpoints are attached.
Vantrongs commentedon Sep 18, 2026
Additional measured case: successful auto-compaction can make the request small enough for WebSockets again, but the running client remains on HTTP until restart. This provides a concrete recovery trigger for this issue, beyond a generic cooldown. Related size/retry report: #38846; image-history report: #43015.
Environment and limits of this report
Observed September 18, 2026: CLI
0.155.0-alpha.9.2as app-server in a locally patched Linux Desktop26.915.31029, NixOS, ChatGPT authentication,gpt-6-astra, built-in OpenAI Responses endpoint. This is not an untouched Desktop installation. No transport/retry modifications were installed during this diagnosis, and no explicit HTTP/HTTPS/ALL proxy environment variables were present in the relevant processes.Runtime evidence is from that version. I also inspected the same session-latched fallback in current stable
rust-v0.155.0and latest prereleaserust-v0.156.0-alpha.2; I have not reproduced the runtime sequence on those two releases.Observed sequence (UTC)
responses_http, despite bodies now being only 9,339,825–9,371,272 bytes before compression (roughly 6.75 MB after zstd). HTTP responses succeeded.responses_websocket, withauth_connection_reused=true.The error shown to the user was:
These are prepared HTTP-body sizes, not measured network traffic or exact WS serialization sizes. The original incident's close code was not preserved by the client, so I cannot claim that its actual close frame was recorded as 1009.
Separately, a bounded synthetic transport probe against the same official endpoint used a minimal valid
response.createwithgenerate:false, padded only with JSON whitespace (no user conversation or screenshots). Results:response.completedresponse.completedpermessage-deflatewas negotiated. This measured boundary is therefore on the decompressed message, not solved simply by enabling WS compression. It is an observed endpoint limit, not a claim about a documented permanent API contract.Code and requested recovery behavior
In current prerelease
ModelClient::force_http_fallback,disable_websockets.swap(true, ...)latches fallback.responses_websocket_enabledthen rejects WS for this client. I found no reset of this flag after compaction; a new client initializes it to false.If the WS message-size policy cannot be increased, please consider one bounded, payload-aware recovery attempt on the next real request after successful compaction, when the complete serialized WS request is eligible again. Preserve valid conversation state; do not reuse a stale
previous_response_idor replay already-executed tools. Require a completed response, not merely a successful handshake, to count recovery as successful. If recovery fails, retain working HTTP with a bounded retry/cooldown policy rather than entering another retry loop. Compaction alone is not proof of eligibility: retained user images can still leave the full request oversized.The initial oversized-request classification belongs to #38846: preserve Close code/reason and avoid repeating an unchanged request after explicit 1009. The additional point here is that a size-induced fallback need not remain permanent after the payload has genuinely shrunk.
Long computer-use sessions repeatedly add screenshots, so this is a plausible recurring workload, not just a large text prompt. Restoring eligible WS continuation could avoid repeated full-history uploads and reduce latency/bandwidth. This report does not establish a controlled performance multiplier or reduced token billing.
Only sanitized aggregate measurements are included; no raw sessions, image contents, account identifiers, credentials, or private project data are attached.