# WebSocket

> Nitro provides cross-platform WebSocket support powered by CrossWS and H3.

WebSocket enables real-time, bidirectional communication between client and server. Nitro's WebSocket integration works across all supported deployment targets including Node.js, Bun, Deno, and Cloudflare Workers.

<read-more title="CrossWS Documentation" to="https://crossws.h3.dev/">



</read-more>

## Enable WebSocket

Enable WebSocket support in your Nitro configuration:

<code-group>

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  features: {
    websocket: true,
  },
});
```

</code-group>

## Usage

Create a WebSocket handler using `defineWebSocketHandler` and export it from a route file. WebSocket handlers follow the same [file-based routing](/docs/routing) as regular request handlers.

```ts [routes/_ws.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    console.log("Connected:", peer.id);
  },
  message(peer, message) {
    console.log("Message:", message.text());
    peer.send("Hello from server!");
  },
  close(peer, details) {
    console.log("Disconnected:", peer.id, details.code, details.reason);
  },
  error(peer, error) {
    console.error("Error:", error);
  },
});
```

<tip>

You can use any route path for WebSocket handlers. For example, `routes/chat.ts` handles WebSocket connections on `/chat`.

</tip>

### Connecting from the client

Use the browser's [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) to connect:

```js
const ws = new WebSocket("ws://localhost:3000/_ws");

ws.addEventListener("open", () => {
  console.log("Connected!");
  ws.send("Hello from client!");
});

ws.addEventListener("message", (event) => {
  console.log("Received:", event.data);
});
```

## Hooks

WebSocket handlers accept the following lifecycle hooks:

### `upgrade`

Called before the WebSocket connection is established. Use it to authenticate requests, set the namespace, or attach context data to the peer.

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    const url = new URL(request.url);
    const token = url.searchParams.get("token");
    if (!isValidToken(token)) {
      throw new Response("Unauthorized", { status: 401 });
    }
    return {
      context: { userId: getUserId(token) },
    };
  },
  open(peer) {
    console.log("User connected:", peer.context.userId);
  },
  // ...
});
```

The `upgrade` hook can return an object with:

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        headers
      </code>
    </td>
    
    <td>
      <code>
        HeadersInit
      </code>
    </td>
    
    <td>
      Response headers to include in the upgrade response
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        namespace
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Override the pub/sub namespace for this connection
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        context
      </code>
    </td>
    
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      Data attached to <code>
        peer.context
      </code>
    </td>
  </tr>
</tbody>
</table>

Throw a `Response` to reject the upgrade.

### `open`

Called when a WebSocket connection is established and the peer is ready to send and receive messages.

```ts
open(peer) {
  peer.send("Welcome!");
}
```

### `message`

Called when a message is received from a peer.

```ts
message(peer, message) {
  const text = message.text();
  const data = message.json();
}
```

### `close`

Called when a WebSocket connection is closed. Receives a `details` object with optional `code` and `reason`.

```ts
close(peer, details) {
  console.log(`Closed: ${details.code} - ${details.reason}`);
}
```

### `error`

Called when an error occurs on the WebSocket connection.

```ts
error(peer, error) {
  console.error("WebSocket error:", error);
}
```

## Peer

The `peer` object represents a connected WebSocket client. It is available in all hooks except `upgrade`.

### Properties

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        id
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Unique identifier for this peer
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        namespace
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Pub/sub namespace this peer belongs to
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        context
      </code>
    </td>
    
    <td>
      <code>
        object
      </code>
    </td>
    
    <td>
      Arbitrary context data set during <code>
        upgrade
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        request
      </code>
    </td>
    
    <td>
      <code>
        Request
      </code>
    </td>
    
    <td>
      The original upgrade request
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        peers
      </code>
    </td>
    
    <td>
      <code>
        Set<Peer>
      </code>
    </td>
    
    <td>
      All connected peers in the same namespace
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        topics
      </code>
    </td>
    
    <td>
      <code>
        Set<string>
      </code>
    </td>
    
    <td>
      Topics this peer is subscribed to
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        remoteAddress
      </code>
    </td>
    
    <td>
      <code>
        string?
      </code>
    </td>
    
    <td>
      Client IP address (adapter-dependent)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        websocket
      </code>
    </td>
    
    <td>
      <code>
        WebSocket
      </code>
    </td>
    
    <td>
      The underlying WebSocket instance
    </td>
  </tr>
</tbody>
</table>

### Methods

#### `peer.send(data, options?)`

Send a message directly to this peer. Accepts strings, objects (serialized as JSON), or binary data.

```ts
peer.send("Hello!");
peer.send({ type: "greeting", text: "Hello!" });
```

#### `peer.subscribe(topic)`

Subscribe this peer to a pub/sub topic.

```ts
peer.subscribe("notifications");
```

#### `peer.unsubscribe(topic)`

Unsubscribe this peer from a topic.

```ts
peer.unsubscribe("notifications");
```

#### `peer.publish(topic, data, options?)`

Broadcast a message to all peers subscribed to a topic within the same namespace. The publishing peer does **not** receive the message.

```ts
peer.publish("chat", { user: "Alice", text: "Hello everyone!" });
```

#### `peer.close(code?, reason?)`

Gracefully close the WebSocket connection.

```ts
peer.close(1000, "Normal closure");
```

#### `peer.terminate()`

Immediately terminate the connection without sending a close frame.

## Message

The `message` object in the `message` hook provides methods to read the incoming data in different formats.

<table>
<thead>
  <tr>
    <th>
      Method
    </th>
    
    <th>
      Return Type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        text()
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      Message as a UTF-8 string
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        json()
      </code>
    </td>
    
    <td>
      <code>
        T
      </code>
    </td>
    
    <td>
      Message parsed as JSON
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        uint8Array()
      </code>
    </td>
    
    <td>
      <code>
        Uint8Array
      </code>
    </td>
    
    <td>
      Message as a byte array
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        arrayBuffer()
      </code>
    </td>
    
    <td>
      <code>
        ArrayBuffer
      </code>
    </td>
    
    <td>
      Message as an ArrayBuffer
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        blob()
      </code>
    </td>
    
    <td>
      <code>
        Blob
      </code>
    </td>
    
    <td>
      Message as a Blob
    </td>
  </tr>
</tbody>
</table>

```ts
message(peer, message) {
  // Parse as text
  const text = message.text();

  // Parse as typed JSON
  const data = message.json<{ type: string; payload: unknown }>();
}
```

## Pub/Sub

Pub/sub (publish/subscribe) enables broadcasting messages to groups of connected peers through topics. Peers subscribe to topics and receive messages published to those topics.

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    peer.subscribe("chat");
    peer.publish("chat", { system: `${peer} joined the chat` });
    peer.send({ system: "Welcome to the chat!" });
  },
  message(peer, message) {
    // Broadcast to all other subscribers
    peer.publish("chat", {
      user: peer.toString(),
      text: message.text(),
    });
    // Echo back to sender
    peer.send({ user: "You", text: message.text() });
  },
  close(peer) {
    peer.publish("chat", { system: `${peer} left the chat` });
  },
});
```

<note>

`peer.publish()` sends the message to all subscribers of the topic **except** the publishing peer. Use `peer.send()` to also send to the publisher.

</note>

### Namespaces

Namespaces provide isolated pub/sub groups for WebSocket connections. Each peer belongs to one namespace, and `peer.publish()` only broadcasts to peers within the same namespace.

By default, the namespace is derived from the request URL pathname. This works naturally with [dynamic routes](/docs/routing#dynamic-routes) — each path gets its own isolated namespace:

```ts [routes/rooms/[room].ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  open(peer) {
    peer.subscribe("messages");
    peer.publish("messages", `${peer} joined ${peer.namespace}`);
  },
  message(peer, message) {
    // Only reaches peers in the same room
    peer.publish("messages", `${peer}: ${message.text()}`);
  },
  close(peer) {
    peer.publish("messages", `${peer} left`);
  },
});
```

In this example, clients connecting to `/rooms/game` are isolated from clients connecting to `/rooms/lobby` — each path is its own namespace.

To override the default namespace, return a custom `namespace` from the `upgrade` hook:

```ts [routes/chat.ts]
import { defineWebSocketHandler } from "nitro";

export default defineWebSocketHandler({
  upgrade(request) {
    // Group connections by a query parameter instead of the pathname
    const url = new URL(request.url);
    const channel = url.searchParams.get("channel") || "general";
    return {
      namespace: `chat:${channel}`,
    };
  },
  open(peer) {
    peer.subscribe("messages");
    peer.publish("messages", `${peer} joined`);
  },
  message(peer, message) {
    peer.publish("messages", `${peer}: ${message.text()}`);
  },
  close(peer) {
    peer.publish("messages", `${peer} left`);
  },
});
```

## Server-Sent Events (SSE)

[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) provide a simpler alternative when you only need server-to-client streaming. Unlike WebSockets, SSE uses standard HTTP and supports automatic reconnection.

```ts [routes/sse.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";

export default defineHandler((event) => {
  const stream = createEventStream(event);

  const interval = setInterval(async () => {
    await stream.push(`Message @ ${new Date().toLocaleTimeString()}`);
  }, 1000);

  stream.onClosed(() => {
    clearInterval(interval);
  });

  return stream.send();
});
```

Connect from the client using the [EventSource API](https://developer.mozilla.org/en-US/docs/Web/API/EventSource):

```js
const source = new EventSource("/sse");

source.onmessage = (event) => {
  console.log(event.data);
};
```

### Structured messages

SSE messages support optional `id`, `event`, and `retry` fields:

```ts [routes/events.ts]
import { defineHandler } from "nitro";
import { createEventStream } from "nitro/h3";

export default defineHandler((event) => {
  const stream = createEventStream(event);
  let id = 0;

  const interval = setInterval(async () => {
    await stream.push({
      id: String(id++),
      event: "update",
      data: JSON.stringify({ value: Math.random() }),
      retry: 3000,
    });
  }, 1000);

  stream.onClosed(() => {
    clearInterval(interval);
  });

  return stream.send();
});
```

<read-more title="H3 Documentation" to="https://h3.dev/">



</read-more>
