How WebSocket Cohosting Works with Multiple Workers in a Single V8 Isolate in celld
WebSocket cohosting in celld enables multiple Workers to share a single V8 isolate, allowing direct message routing through a shared websockets map and eliminating cross-isolate RPC overhead for WebSocket events.
The denoland/celld repository implements a high-performance edge runtime that cohosts multiple Durable Objects (Workers) within the same V8 isolate. This architecture creates a same-isolate fast path where WebSocket connections are managed through a shared state machine accessible to all cohosted Workers.
The CohostedWorker Architecture
Cohosting begins when crates/celld/runtime.rs reserves an idle resident isolate via reserve_isolate. If the isolate already contains a Worker capable of handling the request, the runtime routes the request directly to that Worker without invoking cross-isolate RPC.
The CohostedWorker Struct
Each isolate maintains a Vec<CohostedWorker> that tracks which Workers share the V8 heap. In crates/celld/runtime.rs, the struct is defined as:
pub struct CohostedWorker {
/// The binding name that caused the co-hosting (e.g. "CHAT")
pub target: String,
/// RPC stub used for cross-isolate calls when the Worker is later moved
pub stub: Option<Stub>,
/// The V8 context that runs the Worker's JavaScript
pub v8: V8Context,
}
The v8 field holds the V8 context executing the Worker's JavaScript, while target identifies the binding name that triggered the cohosting relationship.
Cohosting Limits and Enforcement
The runtime enforces a maximum cohosting limit through the CELLD_MAX_COHOSTED environment variable, which defaults to 8 concurrent Workers per isolate. When a Worker attempts to bind to an isolate that has reached this limit, crates/celld/main.rs aborts the operation with a clear error message indicating the cohosting constraint has been exceeded.
WebSocket Registration and Shared State
WebSocket state lives in the isolate's shared websockets map defined in crates/logic/lib.rs. This map stores WebSocketId keys associated with WebSocketKind values, enabling the runtime to track both inbound and outbound connections across all cohosted Workers.
WebSocketKind Enum
The WebSocketKind enum distinguishes between connection types:
pub enum WebSocketKind {
/// Inbound, hibernatable socket (created via `WebSocketPair`)
Regular,
/// Outbound client socket (created via `new WebSocket(url)`)
Outbound,
}
When a Worker calls new WebSocket(url) for an outbound connection or accepts an inbound WebSocketPair, the runtime generates a WebSocketId and registers the appropriate WebSocketKind in the shared map. Because this map lives in the isolate, every cohosted Worker has direct access to the same socket registry without inter-process communication.
Message Routing Without Cross-Isolate RPC
Incoming WebSocket frames are delivered directly to the owning Worker's V8 context by runtime.rs. Because all cohosted Workers share the same isolate, the runtime dispatches events directly to the JavaScript context without serialization overhead.
The WebSocketRouteTiming mechanism guarantees message ordering across the shared state machine. When a socket closes, runtime.rs emits Message::WebSocketClosed { cell, websocket }, which removes the entry from the websockets map and notifies any cohosted Workers holding references to the terminated connection.
Implementation Code Examples
The following examples demonstrate WebSocket cohosting patterns from the celld source repository.
Inbound Hibernatable WebSocket
From examples/wsecho/index.js, this Worker accepts a WebSocketPair and echoes messages:
export default {
async fetch(request, env) {
// Create a pair of sockets that live in the same isolate
const pair = new WebSocketPair();
// Accept the client-facing end
env.state.acceptWebSocket(pair[0]);
// Keep the other end alive in the DO; it survives hibernation
const socket = pair[1];
socket.addEventListener('message', e => socket.send(e.data));
socket.addEventListener('close', () => console.log('closed'));
},
};
Outbound WebSocket from Cohosted Worker
From examples/wsclient/index.js, this Worker initiates an outbound connection while sharing the isolate:
export default {
async fetch(request, env) {
// The Worker runs in the same isolate as other co-hosted Workers
const ws = new WebSocket('wss://example.com/updates');
ws.addEventListener('open', () => console.log('connected'));
ws.addEventListener('message', e => console.log(e.data));
},
};
Summary
- Cohosting allows multiple Workers to share a single V8 isolate in
denoland/celld, eliminating cross-isolate RPC for WebSocket operations. - The
CohostedWorkerstruct incrates/celld/runtime.rstracks cohosted Workers with their V8 contexts and binding targets, limited byCELLD_MAX_COHOSTED(default 8). - WebSocket state is stored in a shared
websocketsmap incrates/logic/lib.rsusingWebSocketKindto differentiate inbound (Regular) and outbound (Outbound) sockets. - Message routing occurs directly to the V8 context without serialization hops, utilizing
WebSocketRouteTimingfor ordering guarantees.
Frequently Asked Questions
What is the maximum number of Workers that can cohost in one isolate?
The default limit is 8 Workers per isolate, configurable via the CELLD_MAX_COHOSTED environment variable. When this limit is reached, crates/celld/main.rs rejects additional binding attempts with a descriptive error message.
How does celld handle WebSocket closing when multiple Workers are cohosted?
When a WebSocket closes, runtime.rs emits a Message::WebSocketClosed event containing the cell and websocket identifiers. The runtime removes the socket from the shared websockets map and propagates the close notification to any cohosted Workers that maintain references to the socket.
What is the difference between Regular and Outbound WebSocketKind?
Regular sockets are inbound, hibernatable connections created via WebSocketPair that persist across Durable Object hibernations. Outbound sockets are client-initiated connections created when a Worker calls new WebSocket(url) to connect to external services.
Does cohosting affect WebSocket message ordering guarantees?
No, cohosting maintains strict ordering through the WebSocketRouteTiming mechanism in runtime.rs. Because all cohosted Workers share the same isolate heap, the runtime can enforce event sequencing without the non-determinism introduced by cross-isolate message passing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →