# How WebSocket Cohosting Works with Multiple Workers in a Single V8 Isolate in celld

> Discover how WebSocket cohosting in celld uses multiple Workers within a single V8 isolate for efficient message routing and reduced RPC overhead. Learn more!

- Repository: [Deno/celld](https://github.com/denoland/celld)
- Tags: internals
- Published: 2026-08-10

---

**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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/crates/celld/runtime.rs), the struct is defined as:

```rust
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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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:

```rust
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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/examples/wsecho/index.js), this Worker accepts a WebSocketPair and echoes messages:

```javascript
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`](https://github.com/denoland/celld/blob/main/examples/wsclient/index.js), this Worker initiates an outbound connection while sharing the isolate:

```javascript
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 `CohostedWorker` struct in [`crates/celld/runtime.rs`](https://github.com/denoland/celld/blob/main/crates/celld/runtime.rs) tracks cohosted Workers with their V8 contexts and binding targets, limited by `CELLD_MAX_COHOSTED` (default 8).
- WebSocket state is stored in a shared `websockets` map in [`crates/logic/lib.rs`](https://github.com/denoland/celld/blob/main/crates/logic/lib.rs) using `WebSocketKind` to differentiate inbound (`Regular`) and outbound (`Outbound`) sockets.
- Message routing occurs directly to the V8 context without serialization hops, utilizing `WebSocketRouteTiming` for 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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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`](https://github.com/denoland/celld/blob/main/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.