Repository navigation
Long CLI threads exceed Responses WebSocket message limit before auto-compaction (close code 1009) #38846
Description
What version of Codex CLI is running?
codex-cli 0.147.0
What subscription do you have?
ChatGPT subscription (exact tier unavailable in the CLI diagnostics)
Which model were you using?
Codex model with a reported 258,400-token context window
What platform is your computer?
Linux 7.0.0-28-generic x86_64 x86_64
What terminal emulator and version are you using (if applicable)?
Linux terminal, no multiplexer relevant to the reproduction
Codex doctor report
Not included because the issue is isolated to one large thread and the WebSocket peer supplied a definitive close code.
What issue are you seeing?
A long CLI thread consistently fails over from the Responses WebSocket transport to HTTPS, while fresh/small threads on the same client and network use WebSockets normally.
The stock CLI reports:
Falling back from WebSockets to HTTPS transport. stream disconnected before completion: websocket closed by server before response.completed
The current model-visible context reported by the client was approximately 225,029 tokens out of 258,400. The observed auto-compaction threshold was 244,800 tokens, so auto-compaction had not yet run.
To identify the otherwise-discarded close-frame details, I built the exact rust-v0.147.0 source and made a diagnostic-only change in responses_websocket.rs to include the received Message::Close(frame) code and reason in the existing error. The same request then reported:
websocket closed by server before response.completed (with code 1009 and reason Utf8Bytes(b""))
RFC 6455 defines close code 1009 as "Message Too Big." This strongly indicates that the serialized request/history exceeds the WebSocket peer's per-message limit before reaching Codex's auto-compaction threshold.
The client retries the apparently unchanged oversized request five times. Every WebSocket attempt closes, after which HTTPS fallback succeeds and the turn completes. This makes long threads slow but still usable.
Fresh threads do not reproduce the issue. A JSON parsing error would normally produce close code 1007 or a protocol/application error rather than 1009.
What steps can reproduce the bug?
- Use or resume a sufficiently long Codex CLI thread. In this reproduction, current model-visible context was about 225k tokens.
- Send a small text-only prompt such as
test again. - Observe five WebSocket reconnect attempts followed by fallback to HTTPS.
- Instrument the existing
Message::Close(frame)branch to retain the close-frame metadata. - Observe peer close code 1009 with an empty reason.
- Send the same prompt in a fresh thread and observe that WebSocket transport works normally.
The raw rollout and terminal captures are intentionally not attached because they contain private project and authentication-related data. The affected rollout was about 5.38 GB cumulatively on disk, but this report does not assume the full rollout is sent; the relevant observation is the 225k model-visible context plus the peer's close code.
What is the expected behavior?
- Measure or budget the serialized WebSocket request before sending it.
- If it exceeds the supported message size, compact first or switch directly to HTTPS.
- Do not retry an unchanged request after receiving close code 1009.
- Preserve and display WebSocket close-frame code/reason in normal diagnostics.
- Keep the WebSocket size threshold and auto-compaction threshold mutually compatible.
Additional information
Possibly related: #32512 documents a 16 MiB WebSocket per-message ceiling in the separate Desktop SSH-handoff path. This report concerns the ordinary CLI Responses sampling path, not artifact transfer.
No installed Codex files were modified. The diagnostic build came from the exact rust-v0.147.0 tag, and only the temporary error formatting was changed so the already-received close frame would be visible.
Activity
jdcodes1 commentedon Aug 17, 2026
Excellent forensics — the patched-build close-code capture pins this. Confirming the code side on
main@ 1f41cc5 and adding the classification/fix seams.The close frame really is discarded. The Responses WebSocket receive loop pattern-matches the close frame away and emits a fixed string:
codex/codex-rs/codex-api/src/endpoint/responses_websocket.rs
Lines 817 to 821 in 1f41cc5
Message::Close(_)→ApiError::Stream("websocket closed by server before response.completed"). Your diagnostic patch (including code + reason) is essentially the first fix as-is — without it, 1009 is indistinguishable from a flaky network drop, which is why the stock CLI's behavior looks so irrational.Why five identical retries follow. The request is serialized and sent as a single
Message::Text(responses_websocket.rs#L882, #L904). Once the failure is anApiError::Stream, the retry machinery treats it like any dropped stream:stream_max_retriessame-transport retries, then the transport fallback (core/src/responses_retry.rs#L86-L100— the gate has no semantic check on the error). For 1009 the payload is byte-for-byte identical on every retry, so all five attempts are deterministic failures; only the HTTPS fallback (no equivalent per-message cap) can succeed. This is the same classification gap as #38831 (response.incomplete→ genericStreamerror → blind retries + fallback): semantically terminal information is flattened into "stream broke".Why compaction never saves you. Auto-compaction triggers on token thresholds (your observed 244,800 of a 258,400 window), but the WS peer's limit is a byte cap on the serialized message. Tokens and bytes correlate loosely at best — histories heavy in JSON structure, tool output, or base64 content hit the byte cap well before the token threshold, opening exactly the window you measured (~225k tokens, under the compaction threshold, over the message cap). No token-tuned threshold can close a byte-defined gap.
Fix outline, smallest first:
WsError::ConnectionClosedseparately from a received close frame — today both collapse into unlabeledStreamerrors (#L567-L569).Your reproduction (small prompt on a ~225k-token thread) is directly reusable as the regression test for (2)/(3): assert one WS attempt max and no user-visible retry storm when the serialized request exceeds the cap.
wegfawefgawefg commentedon Aug 17, 2026
when i instrumented it with heavier logging, it just said it was too big 5 times in a row.
The solution is pretty simple. the client just needs to truncate the message under the cap when it sends it in that's all.
I do not think the right behaviour is to skip retries or instantly default to http requests when the message goes above a certain size.
Hell, subdividing the message would be fine, but if the cap is about throughput anyways, truncation seems right to me.
That being said that call is for OpenAI to make, not me.
I had codex chase a little local patch, but its trivial and openai should probably make one go into mainstream codex-cli on next version.
I would guess a lot of users ran into this same issue and may not even realize it.
jdcodes1 commentedon Aug 17, 2026
Fair pushback on the retry-skipping point — one consideration on truncation, though: the WS message here isn't a stream of independent data, it's one serialized JSON request containing the conversation history. Truncating it under the cap byte-wise would produce invalid JSON (or, if done structurally, silently drop history items the model was supposed to see — changing model behavior with no user-visible signal). The system already has a semantically-safe mechanism for "history too big": compaction, which summarizes rather than drops, and is model-visible. That's why I'd frame the byte-triggered path as "compact early (or choose the transport without the cap)" rather than literal truncation — same trigger you're proposing, but routed through the mechanism that preserves request validity. Subdivision would need server-side reassembly semantics the Responses WS protocol doesn't currently expose. Agreed the final call is OpenAI's; either way the first step is the same one your patch demonstrated — stop discarding the close code.
zsw1234 commentedon Oct 3, 2026
Additional macOS reproduction: successful upgrade, then close 1009 for an image-heavy request
Reproduced on 2026-10-03 with the desktop-bundled codex-cli 0.159.2, GPT-6.1 Sol, and ChatGPT authentication. All controls used the same existing local HTTP proxy route; no node changes or direct-connect comparison were used.
Original desktop symptom: an existing image-heavy conversation established WebSocket connections successfully, retried five times with
websocket closed by server before response.completed, and then completed through HTTPS fallback. The original logs did not preserve the close code.Controls and targeted reproduction:
generate: falseandstore: false. It recorded only event types, sizes, timings, and close-frame metadata.response.completedwith zero output items, in 1,276 ms.The large prewarm reused the images present in the active history before the original failure, but it did not replay the complete original desktop request. This confirms a size-related failure on that endpoint/route, not the exact historical close code or that every reconnect has the same cause. The precise server limit and the component enforcing it were not measured. The diagnostic client offered
permessage-deflate; the negotiated extension and compressed wire size were not recorded.RFC 6455 defines 1009 as a message too large for the receiving endpoint to process. On current
main, the Responses receive loop still discards the close frame asMessage::Close(_)and returns a generic stream error:https://github.com/openai/codex/blob/main/codex-rs/codex-api/src/endpoint/responses_websocket.rs
Suggested narrow fix
Regression coverage could use a mock peer closing with 1009: assert no same-payload WebSocket retry storm, successful HTTP fallback, and unchanged input content. Keep separate coverage for ordinary transient disconnects and unavailable fallback.
This comment contains aggregate diagnostic results only. No private prompts, image data, raw session files, account identifiers, credentials, personal paths, or proxy-provider/node details are attached.
TheOne2006 commentedon Oct 12, 2026
Additional macOS reproduction: boundary bracket around 16 MiB, peer close 1009, then successful HTTP fallback
Tested on 2026-10-13 with the desktop-bundled
codex-cli 0.162.0-alpha.17.2, macOS, and ChatGPT authentication.The additional evidence is a controlled size-boundary test on
wss://chatgpt.com/backend-api/codex/responses, independent of real conversation contents and model inference.Controlled reproduction
Each trial opened a fresh authenticated WebSocket through the same existing local HTTP CONNECT proxy, with normal TLS certificate verification enabled. No proxy settings or nodes were changed between trials. Every handshake succeeded.
The first message was deliberately invalid at the application layer:
It was sent as one unfragmented text message. This lets the endpoint parse/reject small messages without requesting model generation. Only synthetic ASCII padding was sent; no conversation history, images, tool outputs, or private project data were replayed.
The application error was
Expected a 'response.create' message as the first websocket event.It is the expected control result, not a successful inference.16 MiB = 16,777,216 bytes: the last application-parsed control is 80 bytes below that value, and the first rejected message is 1,072 bytes above it. This brackets the observed boundary around 16 MiB; it is not a claim to have measured the exact maximum accepted byte. Each listed size was tested once. Timings include connection setup and upload, rather than measuring a server timeout. A separate trial that collected only a Ping control frame is excluded from this table.Corresponding production observations
Two existing image-heavy conversations showed the stock error and repeated fresh WebSocket handshakes, followed by successful HTTP fallback through the same proxy:
incremental request failed, websocket reuse properties didn't match.In Case B, the previous turn had completed only about 25 seconds before the new request began; prolonged idle time is not required for this failure mode.
The stock production logs omit close-frame metadata, so I cannot prove that every historical close was 1009 or that the HTTP body length exactly equals each WebSocket message length. The synthetic test directly establishes the size-related 1009 behavior on the same endpoint/route; the production sizes, repeatable timing, and working HTTP fallback are consistent with it.
Client recovery gap
The public
rust-v0.162.0-alpha.17source still discards the frame inMessage::Close(_)and returns the genericwebsocket closed by server before response.completedstream error:https://github.com/openai/codex/blob/rust-v0.162.0-alpha.17/codex-rs/codex-api/src/endpoint/responses_websocket.rs#L827-L831
This is the public baseline for the installed
.17.2desktop build, not an assertion that the two builds are byte-identical.Please preserve close code/reason, classify 1009 as non-retryable for an unchanged payload, and immediately use HTTP fallback. A preflight check of the actual serialized WebSocket message bytes could avoid the initial rejection as well. This should complement token-based compaction and preserve useful image/history content, rather than silently truncating it.
No raw logs, rollout files, authentication headers/tokens, account identifiers, local paths, or conversation contents are included in this report.
mirsella commentedon Oct 15, 2026
I’ve hit a closely matching failure in two image-heavy coding threads.
Environment:
codex app-serverBoth threads stopped during context compaction with:
Sending
continueimmediately retriggered compaction and the same failure. Forking one affected conversation only helped temporarily.For one failed attempt, Sleev logged:
The upstream connection was to
wss://chatgpt.com/backend-api/codex/responsesand closed after roughly five seconds. The outgoing payload exceeds the approximately 16 MiB boundary reported above. Our logs did not expose the close code for this request, so I can’t confirm it was 1009.Manual recovery using HTTP/SSE restored progress. Recovery also involved rollback/context handoff, so this wasn’t a controlled comparison of the exact same payload. The failed WebSocket attempts ended in failed turns rather than successful automatic HTTP recovery.
Preserving the upstream close code/reason and falling back to HTTP for oversized requests would make this much easier to diagnose and recover from.