Skip to content

bare websockets - #6932

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

benedikt-bartscher wants to merge 93 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 not alter performance

1 warning

⚠️ 21 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 148 untouched benchmarks
🆕 4 new benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
🆕 test_transport_inbound_socketio N/A 17.1 ms N/A
🆕 test_transport_inbound_websocket N/A 10.2 ms N/A
🆕 test_transport_outbound_socketio N/A 34.5 ms N/A
🆕 test_transport_outbound_websocket N/A 16.9 ms N/A

Comparing benedikt-bartscher:make-sio-optional (b0f9946) with main (656e8c7)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The latest changes appear safe to merge and fix the outstanding hydration-hook issue.

Summary

The PR replaces the default Socket.IO transport with plain WebSockets, adds channels with binary messages, and moves AppHarness to Granian.

  • App events and named channels travel over a plain WebSocket by default.
  • AppHarness runs its embedded servers with Granian.
  • Benchmark drivers compare event workloads over WebSocket and Socket.IO.

Reviews (80) · Last reviewed commit: "Run the Socket.IO boot event through on_..." · 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
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.
Port upstream's repeated-header combining (reflex-dev#7588) from the removed
reflex/app.py helper to _decode_asgi_headers in reflex/event_namespace.py,
which both transports share, keeping the branch's Latin-1 fallback. Its
tests move from tests/units/test_app.py to tests/units/test_event_namespace.py.
Comment thread reflex/socketio_namespace.py Outdated
Granian sets TCP_NODELAY on listeners it binds itself, but the harness
binds its own socket, so with the Redis state manager each update
stalled ~40 ms on delayed ACKs and test_client_side_state raced it.
Also poll the client storage reads that follow an event or navigation.
Port upstream's reconnect back-off (reflex-dev#7605) to the transport abstraction:
retryConnect keeps the branch's addConnectError helper and caps the attempt
count at MAX_CONNECT_ERRORS. Socket.IO reports a transport closed before the
namespace connected as a disconnect, which now retries with back-off; the
plain WebSocket reports it as connect_error, so it gets the same back-off.

The state.js back-off tests run over both transports, with the fake socket
reporting a close before connecting the way each transport does, and read
connect errors through upstream's lazily applied updaters.
@masenf masenf added the roadmap https://github.com/reflex-dev/reflex/issues/2727 label Oct 23, 2026
Port upstream's EventNamespace changes to the moved namespace modules:
the trusted proxy hops for the client IP (reflex-dev#7614) apply in
build_static_router_data, and the cancellation of a disconnected socket's
page-load chains (reflex-dev#7415) runs in BaseEventNamespace.handle_disconnect, so
both transports get them. The branch already decoded non-UTF-8 header
values (reflex-dev#7602). Their tests move from tests/units/test_app.py to
tests/units/test_event_namespace.py, and the transport benchmark's app
double gains the event processor attribute the disconnect reads.

Port the oversized hydrate check of reflex-dev#7551 to the Socket.IO transport only:
state.js wraps the socket.io encoder where it creates the Socket.IO
socket, since the plain WebSocket has no engine.io encoder. Its state.js
tests run over Socket.IO, whose fake socket now encodes CONNECT packets,
and the reconnect back-off of reflex-dev#7605 stays in place for both transports.

uv.lock is upstream's, relocked for the branch's dependencies.
The plain WebSocket sends its hydrate with the connect frame, before any
handshake could announce the server's message limit. A frame over the
limit closed the socket with 1009, which the client reported as a generic
connection failure and retried with back-off forever.

The server now names the limit in that close's reason, and the transport
reports such a close as the hydration_payload_too_large error reflex-dev#7551 added
for Socket.IO, with the frame's size and the limit when the reason gives
it. A reconnect over a limit an earlier handshake announced is refused
before it is sent. retryConnect treats the error as final for both
transports: it records it once, schedules no retry and keeps wait_connect
set, so reconnect() does not rebuild the hydrate, and the toast and banner
keyed on the error code show it.
Comment thread reflex/channels.py Outdated
When a token moved to another socket, the transport dropped the stale
recipient but send_to_token still returned True; the sender now reports
whether it accepted anyone and the fan-out returns that.

@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.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="reflex/event_namespace.py">

<violation number="1" location="reflex/event_namespace.py:1136">
P2: `bool(recipients)` reports success for linked SIDs even when `_deliver` skips the connection or `_Connection.send` rejects it after close. Return the number of recipients actually accepted by `_deliver` (or otherwise check the live connection state) so `send_to_token` does not report a delivery that never reached a socket.</violation>
</file>

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

View guided diff | Re-trigger cubic

Comment thread reflex/event_namespace.py Outdated
# teardown forgets its channel sessions.
connection.close(1008)
self._deliver(recipients, payload, event)
return bool(recipients)

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.

P2: bool(recipients) reports success for linked SIDs even when _deliver skips the connection or _Connection.send rejects it after close. Return the number of recipients actually accepted by _deliver (or otherwise check the live connection state) so send_to_token does not report a delivery that never reached a socket.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At reflex/event_namespace.py, line 1136:

<comment>`bool(recipients)` reports success for linked SIDs even when `_deliver` skips the connection or `_Connection.send` rejects it after close. Return the number of recipients actually accepted by `_deliver` (or otherwise check the live connection state) so `send_to_token` does not report a delivery that never reached a socket.</comment>

<file context>
@@ -1130,6 +1133,7 @@ async def _send_channel_message(
                 # teardown forgets its channel sessions.
                 connection.close(1008)
         self._deliver(recipients, payload, event)
+        return bool(recipients)
 
     async def _send_channel_error(
</file context>

A linked session whose connection was closing, overflowed its send
backlog, or was already gone still counted as a recipient; the
connection now reports whether it queued the frame.

@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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="reflex/event_namespace.py">

<violation number="1" location="reflex/event_namespace.py:437">
P2: `send` reports a failed overflow send after already appending the frame, so the independent writer can still deliver it before teardown cancels the writer. Remove the just-appended frame and decrement `_backlog` before returning `False` to prevent false misses and duplicate retries.</violation>
</file>

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

View guided diff | Re-trigger cubic

Comment thread reflex/event_namespace.py
Whether the frame was queued on a connection that is not closing.
"""
if self.close_code is not None:
return False

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.

P2: send reports a failed overflow send after already appending the frame, so the independent writer can still deliver it before teardown cancels the writer. Remove the just-appended frame and decrement _backlog before returning False to prevent false misses and duplicate retries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At reflex/event_namespace.py, line 437:

<comment>`send` reports a failed overflow send after already appending the frame, so the independent writer can still deliver it before teardown cancels the writer. Remove the just-appended frame and decrement `_backlog` before returning `False` to prevent false misses and duplicate retries.</comment>

<file context>
@@ -424,23 +424,27 @@ def __init__(self, websocket: WebSocket):
         """
         if self.close_code is not None:
-            return
+            return False
         frames = self._frames
         frames.append(frame)
</file context>
Suggested change
return False
frames.pop()
self._backlog -= len(frame)
return False

A frame that overflowed the send backlog was queued before the
connection closed, so the writer could still deliver a send reported
as a miss.
…g it

An app that replaced EventNamespace and overrode on_event saw every
event but the hydrate on connect. The boot event goes through on_event
again, re-raised while booting so a failure still refuses the connect,
and overriding on_event now warns in favor of handle_event, which runs
for every event on both transports.

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 roadmap https://github.com/reflex-dev/reflex/issues/2727

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants