WebSocket
v2.0aWebSocket support with per-connection V8 isolate isolation and Cloudflare Workers compatible API.
Phase 23 — In Progress. Plans 01–04 complete (upgrade detection, relay, ws_messages loop). Plan 05 (WebSocketPair V8 binding) in progress.
Overview
NANO-RS supports WebSocket connections via HTTP Upgrade. Each WebSocket connection runs on a dedicated worker thread that owns a V8 isolate for the duration of the connection. When the connection closes, the isolate is recycled — ensuring no state leaks between clients.
JavaScript handlers use addEventListener (Cloudflare Workers pattern) to register message, close, and error callbacks before the first frame arrives.
Echo Server
export default {
async fetch(request) {
// Reject non-WebSocket requests
if (request.headers.get('Upgrade') !== 'websocket') {
return new Response('Expected WebSocket upgrade', { status: 426 });
}
// Create linked pair: client goes in Response, server stays in handler
const [client, server] = new WebSocketPair();
server.addEventListener('message', (event) => {
// event.data is string for text frames, ArrayBuffer for binary
console.log('Received:', event.data);
server.send(`Echo: ${event.data}`);
});
server.addEventListener('close', (event) => {
console.log('Closed with code', event.code, event.reason);
});
server.addEventListener('error', (event) => {
console.error('Error:', event.message);
});
// Must call accept() before frames are dispatched
server.accept();
return new Response(null, {
status: 101,
webSocket: client,
});
}
};
WebSocketPair
new WebSocketPair() returns two linked WebSocket objects as a destructured array.
const [client, server] = new WebSocketPair(); // client → pass in Response (sent to the browser) // server → keep in handler (send/receive frames)
WebSocket Methods & Properties
| Member | Type | Description |
|---|---|---|
| accept() | method | Accept connection. Must call before send(). |
| send(data) | method | Send text (string) or binary (ArrayBuffer) frame. |
| close(code?, reason?) | method | Send Close frame. Default code 1000. |
| addEventListener(type, fn) | method | Register handler for message, close, or error. |
| readyState | property | 0 CONNECTING · 1 OPEN · 2 CLOSING · 3 CLOSED |
Events
message
Fired when a frame arrives.
| event.data | string for text frames · ArrayBuffer for binary frames |
close
Fired when the connection closes.
| event.code | WebSocket close code (1000 normal, 1001 going away, 1006 abnormal, …) |
| event.reason | Close reason string |
error
Fired on transport error or OOM termination.
| event.message | Error description string |
Limits
Configured in AppLimits / config.json:
| Limit | Default | Config Key | Behavior on Breach |
|---|---|---|---|
| Max concurrent connections | 100 | ws_max_connections | 503 Service Unavailable |
| Max message size | 32 MiB | ws_max_message_bytes | Close 1009 (message too large) |
| Idle timeout | 60 000 ms | ws_idle_timeout_ms | Close 1001 (going away) |
Upgrade Flow
HTTP GET /path
Upgrade: websocket
Connection: Upgrade
│
▼ router.rs — detect_ws_upgrade()
│ axum 101 handshake
▼
tokio relay task — WsChannels
│ WsInbound (client→worker)
│ WsOutbound (worker→client)
▼
TenantPool::dispatch_ws()
│ lazy-spawn WS worker thread
▼
Worker thread:
1. JS fetch() handler called
(registers addEventListener callbacks)
2. 'ws_messages loop starts
recv_timeout(idle_timeout_ms) per frame
3. Per frame:
• CpuTimeoutGuard started
• OOM check
• JS event dispatched (MessageEvent / CloseEvent)
4. On Close or Disconnect:
set_ws_readystate(3)
break 'requests → isolate recycled
Cloudflare Workers Compatibility
NANO-RS WebSocket matches the Cloudflare Workers WebSocket API:
new WebSocketPair()constructorserver.accept()required before sendingaddEventListenerfor event registrationResponsewithwebSocketproperty for upgrade response
cf.webSocket properties.