Skip to content

bare websockets - #6932

Open
benedikt-bartscher wants to merge 84 commits into
reflex-dev:mainfrom
benedikt-bartscher:make-sio-optional
Open

benedikt-bartscher wants to merge 84 commits into
reflex-dev:mainfrom
benedikt-bartscher:make-sio-optional

Conversation

@benedikt-bartscher

@benedikt-bartscher benedikt-bartscher commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

saves ~11.6 KB gz / ~41 KB raw frontend size (a small js chunk replaces whole socketio)
dropped python deps: wsproto, simple-websocket, python-socketio, python-engineio, bidict
new benchmarks in test_event_transport.py show about 2x performance increase compared to socketio

also migrates AppHarness to granian for consistency. otherwise downstream tests using AppHarness still need uvicorn and wsproto/websockets

Review in cubic

@codspeed

codspeed Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ 11 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
❌ 1 regressed benchmark
✅ 144 untouched benchmarks
🆕 4 new benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ test_proxy_list_mutation 9.5 ms 10 ms -4.05%
⚡ test_decode_event_headers[64] 186.2 µs 162.6 µs +14.5%
⚡ test_decode_event_headers[16] 71.6 µs 64.5 µs +11.06%
⚡ test_decode_event_headers[4] 42.5 µs 39.9 µs +6.46%
🆕 test_transport_inbound_socketio N/A 17.6 ms N/A
🆕 test_transport_inbound_websocket N/A 10.2 ms N/A
🆕 test_transport_outbound_socketio N/A 34.4 ms N/A
🆕 test_transport_outbound_websocket N/A 17 ms N/A

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing benedikt-bartscher:make-sio-optional (df90814) with main (5ae5dcd)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Critical impact] The changes since the previous review appear safe to merge.

Summary

This PR replaces the default Socket.IO connection with a plain WebSocket, adds channels with binary attachments, and moves AppHarness to Granian.

  • Reflex apps send state events over a plain WebSocket by default.
  • Apps send named channel messages over the event connection.
  • The benchmark tools compare plain WebSockets with Socket.IO.
  • The embedded AppHarness servers run on Granian.

Reviews (72) · Last reviewed commit: "Match the echo server's handshake to Ref..." · Reviewed by Greptile

Comment thread reflex/event_namespace.py Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review August 22, 2026 22:09
@benedikt-bartscher
benedikt-bartscher requested a review from a team as a code owner August 22, 2026 22:09

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 19 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread reflex/event_namespace.py Outdated
Comment thread reflex/event_namespace.py
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
Comment thread reflex/utils/exec.py Outdated
Comment thread reflex/utils/exec.py Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
@benedikt-bartscher
benedikt-bartscher marked this pull request as draft August 22, 2026 22:27
…ace, awaited disconnect cleanup; revert server-wide ws_max_size
Comment thread reflex/event_namespace.py Outdated
@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review August 22, 2026 22:46

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 17 files

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
Comment thread reflex/event_namespace.py Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js
Comment thread reflex/event_namespace.py Outdated
Comment thread reflex/event_namespace.py Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread reflex/event_namespace.py Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread reflex/testing.py Outdated
Comment thread reflex/utils/exec.py
Comment thread pyproject.toml Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread reflex/testing.py Outdated
Comment thread tests/units/test_testing.py Outdated

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 39 files

Re-trigger cubic

The per-event path reached the ASGI scope through the Socket.IO server on
every frame; the namespace already tracks sessions for disconnect, so hold
the scope there and fall back to the server only for a session on_connect
never saw.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 39 files

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Re-trigger cubic

forget_session drops a session's rooms once, so a handler that resumes after
the socket went away and calls join() put it back where nothing removed it
again and every later fan-out kept addressing the dead connection. join() is
now a no-op for a session whose `open` flag is false, matching what the flag
already tells handlers.
`connect`, `disconnect` and `error` name the client handle's own lifecycle, and
only the server's send path refused them: a client could emit one, and the
transport handed it to on_message as an application message. A handler that
relays what it receives would then answer it with a send the channel API
refuses. `channel.emit` now throws on a reserved name and the transport answers
an inbound one with a `reserved_event` error instead of dispatching it.
handle_connect returns without linking a SID when the query string carries no
token, and logs it at debug because "the transport closes the session" -- which
the plain websocket transport does and the Socket.IO one did not, leaving an
unauthenticated connection open for its ping interval to keep alive. on_connect
now refuses it, undoing its own connection accounting and scope first, since
Socket.IO runs no disconnect handler for a refused connect.
test_uvicorn_worker_carries_the_socket_policy dropped the worker module from
sys.modules and never put it back, so the rest of the session inherited a
module whose CONFIG_KWARGS were baked from the test's environment. It now
restores the cache it found.

test_embedded_server_retries_taken_port polled for a full 15s and then failed on
a bare assert whenever the rebind did not happen. It stops polling once the
server thread is gone and names what went wrong.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 40 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread reflex/socketio_namespace.py Outdated
@masenf masenf added the perf Performance-improving changes label Sep 29, 2026
socket.io-client is now imported only when the Socket.IO transport is
configured, so the playground export splits it into a chunk of its own:
18 chunks instead of 17, with a 10 KB smaller initial bundle.
@masenf masenf added the documentation Improvements or additions to documentation label Oct 6, 2026
@benedikt-bartscher
benedikt-bartscher marked this pull request as draft October 8, 2026 20:12
Transport: each connection queues its frames for a writer task of its own,
so a client that stops reading no longer blocks the coroutines emitting to
it, and is dropped past a 16 MiB backlog. Closing goes through the
session's receive loop, which granian requires to complete a close.
Frames are compact JSON, escape unpaired surrogates and size attachments
in bytes; a huge integer literal is a malformed frame. The event route is
served with and without a trailing slash, which the client now dials,
Socket.IO clients are refused before accept, and emit() without a
recipient broadcasts.

Client: handlers run over a copy and cannot abort the transport's own
bookkeeping, the ping watchdog reports a dead link at once, a channel
connects once, a throwing WebSocket constructor becomes a connect error,
channel names and attachments are validated, an oversized event is
dropped instead of costing the socket, and a socket that cannot be
created is reported as a connection error rather than a rejection that
reconnects without end.

Channels reserve every underscore-prefixed name in both directions and
validate fan-out with nobody listening; registering one where channels
cannot run warns and skips it. A custom sio without a transport setting
falls back to Socket.IO with a deprecation, and reflex.app re-exports the
Socket.IO classes lazily. A plain-string cors_allowed_origins is read as
a list. Auto-detection picks granian when uvicorn has no websocket
library, and granian caps the message size at what it receives.

AppHarness owns its port from construction, reports readiness itself,
fails fast on a busy requested port or a failed lifespan and logs its
errors. The benchmark clients detect the transport the backend speaks.
Docs cover the transport upgrade, proxy paths and channel task pattern.
@benedikt-bartscher
benedikt-bartscher marked this pull request as ready for review October 8, 2026 22:40

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 62 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread packages/reflex-base/news/+socket-deflate-flag.performance.md Outdated
Comment thread packages/reflex-base/news/6932.feature.md Outdated
Comment thread reflex/event_namespace.py Outdated
Comment thread tests/benchmarks/support/idle_clients.py Outdated
Comment thread reflex/socketio_namespace.py Outdated
TRANSPORT in env.json is a name socket.io-client accepts again, as on main,
and a SOCKETIO flag picks the protocol: third-party code hands TRANSPORT to
socket.io-client to share the app's Socket.IO connection. The docs app runs
on transport="socketio" with reflex[socketio], which xy 0.0.7's charts need
until xy rides a channel.

A boot event in a Socket.IO CONNECT packet bypasses on_event, so one that
fails to deserialize refuses the connect instead of disconnecting a session
not yet acknowledged. Header values that are not UTF-8 decode as Latin-1
rather than failing every event on the connection. Idle benchmark clients
poll within the ping timeout. reflex-base's news scopes the deflate setting
to uvicorn and lists the default transport change as breaking.
Port upstream's EventNamespace changes to the moved namespace modules:
the connect-time state schema check now runs in
BaseEventNamespace.handle_connect for both transports (connect_boot_event
becomes connect_auth, which also returns the frontend schema), and the
unknown-substate client_error report uses upstream's wording.

Run client_state_updates.mjs over both the plain WebSocket and Socket.IO
transports; the queue test runtime now serves dynamic imports.
Comment thread reflex/event_namespace.py Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/helpers/websocket.js Outdated
Decode ASGI headers with plain str decodes and fall back to per-header
Latin-1 decoding only when some header is not UTF-8, which is faster than
upstream's decoding. Replace the Socket.IO namespace's _dispatch_event
coroutine with a sync scope lookup, removing a coroutine layer per event,
and drop the router_data type check that its normalization already covers.
A token claimed by another instance drops this socket's token link but
not its connection, so channel sends (rooms, send_to_token,
session.send) kept reaching it until it sent another frame. Deliver
channel messages only to linked sessions and close a stale connection,
whose teardown forgets its channel sessions.

The client flushed its send queue on handshake without checking the limit
that handshake advertises, so a frame queued before the first handshake
or while a socket was closing could make the backend drop the connection.
Check each queued frame and report a dropped one like emit does, or as
message_too_large on its channel.
The benchmark socket client builds on reflex-bench's wire codec, and two
unit tests that also run on Windows imported it. Importing any part of
reflex-bench makes coverage count the whole Linux-only package, whose
own tests are ignored off Linux, which pulled Windows coverage under the
threshold. Run those two tests on Linux only and import the client
inside them.
Both transports now open sessions through BaseEventNamespace: one
connect-and-boot sequence and one handler failure policy. A failed boot
event ends the session on either transport (the plain WebSocket closes
with 1011), so the client retries rather than waiting for a hydrate that
never comes. Undeserializable input ends a session quietly, and other
handler failures are logged within the error budget; Socket.IO's
on_event, on_ping and on_client_error no longer raise into
python-socketio. Socket.IO packets are serialized like plain frames, so
an unpaired surrogate is escaped instead of breaking the send.

Also: one fan-out body for channel room and token sends, a socket policy
read once per connection for both transports, a supports_channels flag
instead of re-reading the config, the embedded test server capped like
the Granian launchers, CORS strings parsed as the environment variable
is, and an unreachable uvicorn warning removed. The Socket.IO namespace
tests move from test_app.py to test_socketio_namespace.py, and the
namespace tests share fixtures and a serve() helper.
websocket.js settles a closed connection in one place, flushes the
channel and transport queues through one helper, and checks and reports
oversized frames through one size check and one message. The
single-caller emitChannel, which bypassed the channel's guards, is gone,
and the generic JSON helpers move to json.js. state.js shares its
warm-socket, socket disposal and connect-error helpers and drops an
unreachable check.

The client-only tests that ran websocket.js through the Python harness
move to websocket_js.test.mjs, and the channels integration test now
queues its frame on the transport, as its name says.
The benchmark socket client detects a backend's protocol once instead of
per connection, so Socket.IO reconnect timings no longer include a
failed WebSocket upgrade per client, and codecs carry their bare connect
and ping frames instead of callers branching on the protocol.
reflex-bench shares its process spawn, stop and report handling, its
worker wrapper and small session helpers, and drift tests pin the
protocol constants it copies from Reflex. The bench tests share their
fixtures through factories.py.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 31 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread tests/units/reflex_bench/drivers/test_echo_server.py Outdated
Comment thread tests/benchmarks/support/socket_client.py Outdated
The echo server announced its generator's 16 MiB frame limit as its
message size, while a default Reflex app (and its own Socket.IO
greeting) announces 1 MB; the drift test only compared keys. It now
announces Reflex's default on both protocols, and the test compares the
whole handshake against a default-configured app.

detect_protocol always waited up to 30 seconds to open its websocket, so
the benchmark socket client's first connection to a hung backend ignored
the caller's timeout. Detection now takes the timeout, and the client
passes its own through.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation perf Performance-improving changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants