---
description: Handle real-time WebSocket connections, messages, broadcasts, and lifecycle hooks in the Agents SDK.
title: WebSockets
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt  
> Use this file to discover all available pages before exploring further.

# WebSockets

Last updated Jun 3, 2026|Copy as Markdown|[View as Markdown](https://developers.cloudflare.com/agents/runtime/communication/websockets/index.md)|[Agent setup](https://developers.cloudflare.com/agent-setup/)

Agents support WebSocket connections for real-time, bi-directional communication. This page covers server-side WebSocket handling. For client-side connection, refer to the [Client SDK](https://developers.cloudflare.com/agents/communication-channels/chat/client-sdk/).

## Lifecycle hooks

Agents have several lifecycle hooks that fire at different points:

| Hook                                        | When called                                                                                |
| ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| onStart(props?)                             | Once when the agent first starts (before any connections)                                  |
| onRequest(request)                          | When an HTTP request is received (non-WebSocket)                                           |
| onConnect(connection, ctx)                  | When a new WebSocket connection is established                                             |
| onMessage(connection, message)              | When a WebSocket message is received                                                       |
| onClose(connection, code, reason, wasClean) | When a WebSocket connection closes                                                         |
| onError(connection, error)                  | When a WebSocket error occurs on a connection                                              |
| onError(error)                              | When a server-level error occurs (not tied to a specific connection)                       |
| shouldSendProtocolMessages(connection, ctx) | Whether to send protocol messages (identity, state, MCP) to this connection. Default: true |

### `onStart`

`onStart()` is called once when the agent first starts, before any connections are established:

```js
export class MyAgent extends Agent {
	async onStart() {
		// Initialize resources
		console.log(`Agent ${this.name} starting...`);

		// Load data from storage
		const savedData = this.sql`SELECT * FROM cache`;
		for (const row of savedData) {
			// Rebuild in-memory state from persistent storage
		}
	}

	onConnect(connection) {
		// By the time connections arrive, onStart has completed
	}
}
```

```ts
export class MyAgent extends Agent {
	async onStart() {
		// Initialize resources
		console.log(`Agent ${this.name} starting...`);

		// Load data from storage
		const savedData = this.sql`SELECT * FROM cache`;
		for (const row of savedData) {
			// Rebuild in-memory state from persistent storage
		}
	}

	onConnect(connection: Connection) {
		// By the time connections arrive, onStart has completed
	}
}
```

## Handling connections

Define `onConnect` and `onMessage` methods on your Agent to accept WebSocket connections:

```js
import { Agent, Connection, ConnectionContext, WSMessage } from "agents";

export class ChatAgent extends Agent {
	async onConnect(connection, ctx) {
		// Connections are automatically accepted
		// Access the original request for auth, headers, cookies
		const url = new URL(ctx.request.url);
		const token = url.searchParams.get("token");

		if (!token) {
			connection.close(4001, "Unauthorized");
			return;
		}

		// Store user info on this connection
		connection.setState({ authenticated: true });
	}

	async onMessage(connection, message) {
		if (typeof message === "string") {
			// Handle text message
			const data = JSON.parse(message);
			connection.send(JSON.stringify({ received: data }));
		}
	}
}
```

```ts
import { Agent, Connection, ConnectionContext, WSMessage } from "agents";

export class ChatAgent extends Agent {
	async onConnect(connection: Connection, ctx: ConnectionContext) {
		// Connections are automatically accepted
		// Access the original request for auth, headers, cookies
		const url = new URL(ctx.request.url);
		const token = url.searchParams.get("token");

		if (!token) {
			connection.close(4001, "Unauthorized");
			return;
		}

		// Store user info on this connection
		connection.setState({ authenticated: true });
	}

	async onMessage(connection: Connection, message: WSMessage) {
		if (typeof message === "string") {
			// Handle text message
			const data = JSON.parse(message);
			connection.send(JSON.stringify({ received: data }));
		}
	}
}
```

## Connection object

Each connected client has a unique `Connection` object:

| Property/Method       | Type                | Description                                                                             |
| --------------------- | ------------------- | --------------------------------------------------------------------------------------- |
| id                    | string              | Unique identifier for this connection                                                   |
| uri                   | string \| null      | URL of the original WebSocket upgrade request. Persists across hibernation              |
| state                 | State               | Per-connection state object                                                             |
| setState(state)       | void                | Update connection state                                                                 |
| send(message)         | void                | Send message to this client                                                             |
| close(code?, reason?) | void                | Close the connection                                                                    |
| tags                  | readonly string\[\] | Tags assigned via getConnectionTags. Always includes the connection ID as the first tag |
| server                | string              | The agent instance name (same as this.name on the Agent)                                |

### Per-connection state

Store data specific to each connection (user info, preferences, etc.):

```js
export class ChatAgent extends Agent {
	async onConnect(connection, ctx) {
		const userId = new URL(ctx.request.url).searchParams.get("userId");

		connection.setState({
			userId: userId || "anonymous",
			role: "user",
			joinedAt: Date.now(),
		});
	}

	async onMessage(connection, message) {
		// Access connection-specific state
		console.log(`Message from ${connection.state.userId}`);
	}
}
```

```ts
interface ConnectionState {
	userId: string;
	role: "admin" | "user";
	joinedAt: number;
}

export class ChatAgent extends Agent {
	async onConnect(
		connection: Connection<ConnectionState>,
		ctx: ConnectionContext,
	) {
		const userId = new URL(ctx.request.url).searchParams.get("userId");

		connection.setState({
			userId: userId || "anonymous",
			role: "user",
			joinedAt: Date.now(),
		});
	}

	async onMessage(connection: Connection<ConnectionState>, message: WSMessage) {
		// Access connection-specific state
		console.log(`Message from ${connection.state.userId}`);
	}
}
```

## Broadcasting to all clients

Use `this.broadcast()` to send a message to all connected clients:

```js
export class ChatAgent extends Agent {
	async onMessage(connection, message) {
		// Broadcast to all connected clients
		this.broadcast(
			JSON.stringify({
				from: connection.id,
				message: message,
				timestamp: Date.now(),
			}),
		);
	}

	// Broadcast from any method
	async notifyAll(event, data) {
		this.broadcast(JSON.stringify({ event, data }));
	}
}
```

```ts
export class ChatAgent extends Agent {
	async onMessage(connection: Connection, message: WSMessage) {
		// Broadcast to all connected clients
		this.broadcast(
			JSON.stringify({
				from: connection.id,
				message: message,
				timestamp: Date.now(),
			}),
		);
	}

	// Broadcast from any method
	async notifyAll(event: string, data: unknown) {
		this.broadcast(JSON.stringify({ event, data }));
	}
}
```

### Excluding connections

Pass an array of connection IDs to exclude from the broadcast:

```js
// Broadcast to everyone except the sender
this.broadcast(
	JSON.stringify({ type: "user-typing", userId: "123" }),
	[connection.id], // Do not send to the originator
);
```

```ts
// Broadcast to everyone except the sender
this.broadcast(
	JSON.stringify({ type: "user-typing", userId: "123" }),
	[connection.id], // Do not send to the originator
);
```

## Connection tags

Tag connections for easy filtering. Override `getConnectionTags()` to assign tags when a connection is established:

```js
export class ChatAgent extends Agent {
	getConnectionTags(connection, ctx) {
		const url = new URL(ctx.request.url);
		const role = url.searchParams.get("role");

		const tags = [];
		if (role === "admin") tags.push("admin");
		if (role === "moderator") tags.push("moderator");

		return tags; // Up to 9 tags, max 256 chars each
	}

	// Later, broadcast only to admins
	notifyAdmins(message) {
		for (const conn of this.getConnections("admin")) {
			conn.send(message);
		}
	}
}
```

```ts
export class ChatAgent extends Agent {
	getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
		const url = new URL(ctx.request.url);
		const role = url.searchParams.get("role");

		const tags: string[] = [];
		if (role === "admin") tags.push("admin");
		if (role === "moderator") tags.push("moderator");

		return tags; // Up to 9 tags, max 256 chars each
	}

	// Later, broadcast only to admins
	notifyAdmins(message: string) {
		for (const conn of this.getConnections("admin")) {
			conn.send(message);
		}
	}
}
```

### Connection management methods

| Method                      | Signature                               | Description                                                                                                               |
| --------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| getConnections              | (tag?: string) => Iterable<Connection>  | Get all connections, optionally by tag                                                                                    |
| getConnection               | (id: string) => Connection \| undefined | Get connection by ID                                                                                                      |
| getConnectionTags           | (connection, ctx) => string\[\]         | Override to tag connections                                                                                               |
| broadcast                   | (message, without?: string\[\]) => void | Send to all connections                                                                                                   |
| isConnectionReadonly        | (connection) => boolean                 | Check if a connection is [readonly](https://developers.cloudflare.com/agents/runtime/communication/readonly-connections/) |
| isConnectionProtocolEnabled | (connection) => boolean                 | Check if protocol messages are enabled for this connection                                                                |

## Handling binary data

Messages can be strings or binary (`ArrayBuffer` / `ArrayBufferView`):

```js
export class FileAgent extends Agent {
	async onMessage(connection, message) {
		if (message instanceof ArrayBuffer) {
			// Handle binary upload
			const bytes = new Uint8Array(message);
			await this.processFile(bytes);
			connection.send(
				JSON.stringify({ status: "received", size: bytes.length }),
			);
		} else if (typeof message === "string") {
			// Handle text command
			const command = JSON.parse(message);
			// ...
		}
	}
}
```

```ts
export class FileAgent extends Agent {
	async onMessage(connection: Connection, message: WSMessage) {
		if (message instanceof ArrayBuffer) {
			// Handle binary upload
			const bytes = new Uint8Array(message);
			await this.processFile(bytes);
			connection.send(
				JSON.stringify({ status: "received", size: bytes.length }),
			);
		} else if (typeof message === "string") {
			// Handle text command
			const command = JSON.parse(message);
			// ...
		}
	}
}
```

Note

Agents automatically send JSON text frames (identity, state, MCP servers) to every connection. If your client only handles binary data and cannot process these frames, use [shouldSendProtocolMessages](https://developers.cloudflare.com/agents/runtime/communication/protocol-messages/) to suppress them.

## Error and close handling

Handle connection errors and disconnections. The `onError` method has two overloads — one for WebSocket connection errors and one for server-level errors:

```js
export class ChatAgent extends Agent {
	// WebSocket connection error

	// Server-level error (not tied to a specific connection)

	onError(connectionOrError, error) {
		if (error) {
			console.error(`Connection ${connectionOrError.id} error:`, error);
		} else {
			console.error("Server error:", connectionOrError);
		}
	}

	async onClose(connection, code, reason, wasClean) {
		console.log(`Connection ${connection.id} closed: ${code} ${reason}`);

		this.broadcast(
			JSON.stringify({
				event: "user-left",
				userId: connection.state?.userId,
			}),
		);
	}
}
```

```ts
export class ChatAgent extends Agent {
	// WebSocket connection error
	onError(connection: Connection, error: unknown): void;
	// Server-level error (not tied to a specific connection)
	onError(error: unknown): void;
	onError(connectionOrError: Connection | unknown, error?: unknown) {
		if (error) {
			console.error(
				`Connection ${(connectionOrError as Connection).id} error:`,
				error,
			);
		} else {
			console.error("Server error:", connectionOrError);
		}
	}

	async onClose(
		connection: Connection,
		code: number,
		reason: string,
		wasClean: boolean,
	) {
		console.log(`Connection ${connection.id} closed: ${code} ${reason}`);

		this.broadcast(
			JSON.stringify({
				event: "user-left",
				userId: connection.state?.userId,
			}),
		);
	}
}
```

The default `onError` implementation logs the error and rethrows it. Override it to add custom error handling, reporting, or recovery logic.

## Message types

| Type            | Description                     |
| --------------- | ------------------------------- |
| string          | Text message (typically JSON)   |
| ArrayBuffer     | Binary data                     |
| ArrayBufferView | Typed array view of binary data |

## Hibernation

Agents support hibernation — they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections.

### Enabling hibernation

Hibernation is enabled by default. To disable:

```js
export class AlwaysOnAgent extends Agent {
	static options = { hibernate: false };
}
```

```ts
export class AlwaysOnAgent extends Agent {
	static options = { hibernate: false };
}
```

### How hibernation works

1. Agent is active, handling connections
2. After a period of inactivity with no messages, the agent hibernates (sleeps)
3. WebSocket connections remain open (handled by Cloudflare)
4. When a message arrives, the agent wakes up
5. `onMessage` is called as normal

### What persists across hibernation

| Persists                 | Does not persist    |
| ------------------------ | ------------------- |
| this.state (agent state) | In-memory variables |
| connection.state         | Timers/intervals    |
| SQLite data (this.sql)   | Promises in flight  |
| Connection metadata      | Local caches        |

Store important data in `this.state` or SQLite, not in class properties:

```js
export class MyAgent extends Agent {
	initialState = { counter: 0 };

	// Do not do this - lost on hibernation
	localCounter = 0;

	onMessage(connection, message) {
		// Persists across hibernation
		this.setState({ counter: this.state.counter + 1 });

		// Lost after hibernation
		this.localCounter++;
	}
}
```

```ts
export class MyAgent extends Agent<Env, { counter: number }> {
	initialState = { counter: 0 };

	// Do not do this - lost on hibernation
	private localCounter = 0;

	onMessage(connection: Connection, message: WSMessage) {
		// Persists across hibernation
		this.setState({ counter: this.state.counter + 1 });

		// Lost after hibernation
		this.localCounter++;
	}
}
```

## Common patterns

### Presence tracking

Track who is online using per-connection state. Connection state is automatically cleaned up when users disconnect:

```js
export class PresenceAgent extends Agent {
	onConnect(connection, ctx) {
		const url = new URL(ctx.request.url);
		const name = url.searchParams.get("name") || "Anonymous";

		connection.setState({
			name,
			joinedAt: Date.now(),
			lastSeen: Date.now(),
		});

		// Send current presence to new user
		connection.send(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);

		// Notify others that someone joined
		this.broadcastPresence();
	}

	onClose(connection) {
		// No manual cleanup needed - connection state is automatically gone
		this.broadcastPresence();
	}

	onMessage(connection, message) {
		if (message === "ping") {
			connection.setState((prev) => ({
				...prev,
				lastSeen: Date.now(),
			}));
			connection.send("pong");
		}
	}

	getPresence() {
		const users = {};
		for (const conn of this.getConnections()) {
			if (conn.state) {
				users[conn.id] = {
					name: conn.state.name,
					lastSeen: conn.state.lastSeen,
				};
			}
		}
		return users;
	}

	broadcastPresence() {
		this.broadcast(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);
	}
}
```

```ts
type UserState = {
	name: string;
	joinedAt: number;
	lastSeen: number;
};

export class PresenceAgent extends Agent {
	onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {
		const url = new URL(ctx.request.url);
		const name = url.searchParams.get("name") || "Anonymous";

		connection.setState({
			name,
			joinedAt: Date.now(),
			lastSeen: Date.now(),
		});

		// Send current presence to new user
		connection.send(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);

		// Notify others that someone joined
		this.broadcastPresence();
	}

	onClose(connection: Connection) {
		// No manual cleanup needed - connection state is automatically gone
		this.broadcastPresence();
	}

	onMessage(connection: Connection<UserState>, message: WSMessage) {
		if (message === "ping") {
			connection.setState((prev) => ({
				...prev!,
				lastSeen: Date.now(),
			}));
			connection.send("pong");
		}
	}

	private getPresence() {
		const users: Record<string, { name: string; lastSeen: number }> = {};
		for (const conn of this.getConnections<UserState>()) {
			if (conn.state) {
				users[conn.id] = {
					name: conn.state.name,
					lastSeen: conn.state.lastSeen,
				};
			}
		}
		return users;
	}

	private broadcastPresence() {
		this.broadcast(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);
	}
}
```

### Chat room with broadcast

```js
export class ChatRoom extends Agent {
	onConnect(connection, ctx) {
		const url = new URL(ctx.request.url);
		const username = url.searchParams.get("username") || "Anonymous";

		connection.setState({ username });

		// Notify others
		this.broadcast(
			JSON.stringify({
				type: "join",
				user: username,
				timestamp: Date.now(),
			}),
			[connection.id], // Do not send to the joining user
		);
	}

	onMessage(connection, message) {
		if (typeof message !== "string") return;

		const { username } = connection.state;

		this.broadcast(
			JSON.stringify({
				type: "message",
				user: username,
				text: message,
				timestamp: Date.now(),
			}),
		);
	}

	onClose(connection) {
		const { username } = connection.state || {};
		if (username) {
			this.broadcast(
				JSON.stringify({
					type: "leave",
					user: username,
					timestamp: Date.now(),
				}),
			);
		}
	}
}
```

```ts
type Message = {
	type: "message" | "join" | "leave";
	user: string;
	text?: string;
	timestamp: number;
};

export class ChatRoom extends Agent {
	onConnect(connection: Connection, ctx: ConnectionContext) {
		const url = new URL(ctx.request.url);
		const username = url.searchParams.get("username") || "Anonymous";

		connection.setState({ username });

		// Notify others
		this.broadcast(
			JSON.stringify({
				type: "join",
				user: username,
				timestamp: Date.now(),
			} satisfies Message),
			[connection.id], // Do not send to the joining user
		);
	}

	onMessage(connection: Connection, message: WSMessage) {
		if (typeof message !== "string") return;

		const { username } = connection.state as { username: string };

		this.broadcast(
			JSON.stringify({
				type: "message",
				user: username,
				text: message,
				timestamp: Date.now(),
			} satisfies Message),
		);
	}

	onClose(connection: Connection) {
		const { username } = (connection.state as { username: string }) || {};
		if (username) {
			this.broadcast(
				JSON.stringify({
					type: "leave",
					user: username,
					timestamp: Date.now(),
				} satisfies Message),
			);
		}
	}
}
```

## Suppressing protocol messages

By default, agents send JSON text frames (identity, state sync, MCP server lists) to every connection. Override `shouldSendProtocolMessages` to suppress them for specific connections — for example, binary-only clients that cannot handle JSON text frames:

```js
export class IoTAgent extends Agent {
	shouldSendProtocolMessages(connection, ctx) {
		const url = new URL(ctx.request.url);
		return url.searchParams.get("protocol") !== "binary";
	}
}
```

```ts
export class IoTAgent extends Agent {
	shouldSendProtocolMessages(
		connection: Connection,
		ctx: ConnectionContext,
	): boolean {
		const url = new URL(ctx.request.url);
		return url.searchParams.get("protocol") !== "binary";
	}
}
```

When this returns `false`, the connection does not receive identity, state, or MCP server list frames — neither on connect nor via broadcasts. The connection can still send and receive regular messages, use RPC, and participate in all non-protocol communication.

Use `isConnectionProtocolEnabled(connection)` to check the status of any connection at runtime.

## Agent properties

These properties are available on `this` inside any Agent method:

| Property   | Type               | Description                                                               |
| ---------- | ------------------ | ------------------------------------------------------------------------- |
| this.name  | string             | The instance name of this agent                                           |
| this.state | State              | The current agent state (lazy-loaded from SQLite)                         |
| this.env   | Env                | Worker environment bindings                                               |
| this.ctx   | DurableObjectState | Durable Object context (storage, alarms, etc.)                            |
| this.sql   | template tag       | SQL template tag for executing queries against the agent's SQLite storage |
| this.mcp   | MCPClientManager   | MCP client manager for connecting to external MCP servers                 |

## Connecting from clients

For browser connections, use the Agents client SDK:

* **Vanilla JS**: `AgentClient` from `agents/client`
* **React**: `useAgent` hook from `agents/react`

Refer to [Client SDK](https://developers.cloudflare.com/agents/communication-channels/chat/client-sdk/) for full documentation.

## Next steps

### [State synchronization](https://developers.cloudflare.com/agents/runtime/lifecycle/state/)

Sync state between agents and clients.

### [Callable methods](https://developers.cloudflare.com/agents/runtime/lifecycle/callable-methods/)

RPC over WebSockets for method calls.

### [Cross-domain authentication](https://developers.cloudflare.com/agents/runtime/operations/cross-domain-authentication/)

Secure WebSocket connections across domains.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.DMYpXs3t.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/communication/websockets/#page","headline":"WebSockets · Cloudflare Agents docs","description":"Handle real-time WebSocket connections, messages, broadcasts, and lifecycle hooks in the Agents SDK.","url":"https://developers.cloudflare.com/agents/runtime/communication/websockets/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-06-03","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
