uNetworking / uNetworking/uWebSockets

io_uring backend silently drops WebSocket messages above ~2KB( works reliably on epoll)

Open
#1,931 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
19k
Forks
1.9k
Avg merge
3h 24m
Merged PRs (30d)
5

Description

Environment
  • uSockets commit: 86097c490263ab662d62e8e7b541390bdec7d149
  • liburing: liburing-2.6 tag
  • OS: Ubuntu 22.04 (container), 6.8.0-124-generic
  • Build: compiled with LIBUS_USE_IO_URING, linked against liburing 2.6
  • Server: uWebSockets App/ws (C++), single .message handler, compression = uWS::DISABLED
Summary

On the io_uring backend, WebSocket binary messages above roughly 2–5KB are silently lost, they never reach the .message callback at all (confirmed via unconditional std::cerr logging at the very top of the callback, before any parsing). Smaller messages (a few hundred bytes to ~1.4KB) sent on the same connection, interleaved with the large ones, are delivered reliably every time.

Switching the exact same server code to the epoll backend (rebuilding uSockets without LIBUS_USE_IO_URING / WITH_IO_URING) makes the large messages arrive consistently, 100% of the time in repeated testing. No other code changed.

Repro details

  • Client: browser WebSocket (binaryType = 'arraybuffer'), sends 3 binary frames in quick succession on one connection: ~1.3KB, ~5KB, ~200B (gzip payloads with a 1-byte transport-tag prefix).
  • Confirmed via Chrome DevTools Network → WS frames tab that all 3 frames leave the browser correctly, including the ~5KB one.
  • Server-side: added an unconditional debug print as the very first statement inside the .message handler's downstream processing (before any parsing/decompression), dumping message size + first bytes to stderr.
  • Result with LIBUS_USE_IO_URING: only the ~1.3KB and ~200B messages ever print. The ~5KB message never appears in the log at all — not truncated, not corrupted, just entirely absent, as if .message was never invoked for that frame.
  • Result on epoll (same binary, same client, same test): all 3 messages, including the ~5KB one, print consistently across repeated runs.
Expected behavior

All WebSocket frames within maxPayloadLength should reach the .message callback regardless of backend, the same as they do on epoll.

Additional notes
  • maxPayloadLength was set well above the message size (1MB), so this isn't a payload-length rejection — a rejection would also normally close the connection with 1009, which did not happen here (connection stayed open, the surrounding smaller messages kept flowing fine).
  • This looks like it could be related to io_uring's registered/ring-mapped buffer handling and reassembly of a socket read that spans more than one ring buffer, but I haven't traced it in the uSockets io_uring backend source myself

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce with the listed Ubuntu, liburing 2.6, uSockets commit, and three-frame browser test, then compare io_uring with epoll. Start in the uSockets io_uring backend, focusing on registered or ring-mapped buffers and reassembly when a read spans buffers. Done means every frame within maxPayloadLength reaches the WebSocket message callback, with coverage for the larger frame.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, ubuntu
Domain
backend, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.