Skip to content

fix(openai): recover from stale pooled Responses WebSocket connections#6523

Open
longcw wants to merge 2 commits into
mainfrom
longc/responses-ws-stale-conn
Open

fix(openai): recover from stale pooled Responses WebSocket connections#6523
longcw wants to merge 2 commits into
mainfrom
longc/responses-ws-stale-conn

Conversation

@longcw

@longcw longcw commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #6513.

Problem

The Responses WebSocket transport pools and reuses connections. A pooled socket closed while idle (by OpenAI or a NAT/proxy) is not reflected in ws.closed until a read observes it, so it fails on the next send_str() — burning a full outer LLM retry (with backoff) per stale socket, adding seconds of latency to a voice turn and eventually failing once enough accumulate.

Fix

When a reused connection fails on send, discard it and try the next instead of raising a retryable error. A freshly created connection failing to send is still raised, and a connection returns to the pool only on clean completion. Also sets a WebSocket heartbeat so idle sockets stay warm and dead peers are detected.

Alternatives considered

#6514 / #6515 add validate_cb=lambda ws: not ws.closed, but that is ineffective — aiohttp keeps ws.closed False on an idle drop, so the check hands a dead socket straight through (verified against aiohttp 3.14). Opened as a new PR since the mechanism differs: react on send vs. validate on acquire.

A pooled Responses socket closed while idle isn't reflected in ws.closed until a read observes it, so it fails on the next send and burns a full outer LLM retry per stale socket.

Add a ws heartbeat to keep idle connections warm and detect dead peers, and reconnect in place when a reused connection fails on send instead of raising a retryable error; a fresh connection failing to send is still raised.
@longcw
longcw requested a review from a team as a code owner July 23, 2026 06:14

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@chenghao-mou chenghao-mou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, one small question about cancellation.

ws = await self._pool.get(timeout=self._timeout)
reused = self._pool.last_connection_reused
try:
await ws.send_str(data)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the socket when cancelled as well?

except Exception in _acquire_and_send didn't catch CancelledError, so a cancellation during send_str left the acquired socket orphaned in the pool.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI Responses WebSocket pool may reuse stale connections after concurrent requests

2 participants