Skip to content

fix(client): give up on a websocket write the browser doesn't take - #644

Open
guigs wants to merge 1 commit into
rubycdp:mainfrom
guigs:fix/bound-websocket-write
Open

guigs wants to merge 1 commit into
rubycdp:mainfrom
guigs:fix/bound-websocket-write

Conversation

@guigs

@guigs guigs commented Oct 12, 2026

Copy link
Copy Markdown

A browser that stops reading its websocket freezes every thread using the client, with no error and no timeout. WebSocket#send_message holds @driver_mutex across a blocking @sock.write, and the command timeout (pending.value!(timeout)) only starts once that write returns. When the browser stops reading and a frame is bigger than the kernel socket buffers can hold, the sending thread blocks in IO#write forever. Every other command then blocks on the mutex before it is even sent, so its timeout never starts either. Browser#quit hangs the same way, because Client#close → WebSocket#close needs the same mutex.

We hit this in production: a Sidekiq process whose 6 threads share one browser to render PDFs froze for hours. Its threads sat idle, it logged nothing, and its health checks stayed green. It's reproducible by sending a SIGSTOP to Chrome and then sending a multi-MB command. On my machine the threshold was between 1.3MB and 2.5MB, and it depends on the kernel's socket buffer settings.

The write now uses write_nonblock and waits up to the command's timeout for the socket to take more data. The wait restarts whenever some bytes are accepted, so a slow link that is still moving isn't affected. If the browser takes nothing for that long, the connection is closed, because the frame was cut off midway, and DeadBrowserError is raised. Commands queued behind it fail the same way through the existing dead-connection path. The handshake, close and pong frames aren't tied to a command and keep the old unbounded write; they are a few bytes each.

WebSocket#send_message takes the timeout as an optional positional argument, so existing calls like send_message(id: 1) keep working.

A browser that stops reading its socket (hung, or stopped) blocked the
sending thread forever in IO#write, before the command's timeout even
started, while holding the lock every other command needs to be sent.
The whole client froze: no error, no timeout, threads asleep.

Write with write_nonblock and wait up to the command's timeout for the
socket to take more data. If none is accepted, close the connection,
since the frame was cut off midway, and raise DeadBrowserError; the
commands waiting behind it fail the same way.
@guigs
guigs force-pushed the fix/bound-websocket-write branch from 6fd81f8 to f7043fe Compare October 12, 2026 13:39
@guigs
guigs marked this pull request as ready for review October 12, 2026 13:40
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.

1 participant