Skip to content

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

Description

@cap-code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions