WebSockets (2)·续
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:
export class AlwaysOnAgent extends Agent {
static options = { hibernate: false };
}
export class AlwaysOnAgent extends Agent {
static options = { hibernate: false };
}
How hibernation works
- Agent is active, handling connections
- After a period of inactivity with no messages, the agent hibernates (sleeps)
- WebSocket connections remain open (handled by Cloudflare)
- When a message arrives, the agent wakes up
onMessageis 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:
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++;
}
}
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:
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(),
}),
);
}
}
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
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(),
}),
);
}
}
}
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:
export class IoTAgent extends Agent {
shouldSendProtocolMessages(connection, ctx) {
const url = new URL(ctx.request.url);
return url.searchParams.get("protocol") !== "binary";
}
}
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:
AgentClientfromagents/client - React:
useAgenthook fromagents/react
Refer to Client SDK for full documentation.
Next steps
State synchronization
Sync state between agents and clients.
Callable methods
RPC over WebSockets for method calls.
Cross-domain authentication
Secure WebSocket connections across domains.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/communication/websockets/#page","headline":"WebSockets","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/agents/runtime/communication/websockets/og.png?v=b59d4260b0dd4fe9","dateModified":"2026-06-03","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Give an agent a catalog of on-demand instructions, resources, and scripts with agents/skills, activated by the model only when a task matches. title: Agent Skills image: https://developers.cloudflare.com/agents/runtime/execution/agent-skills/og.png?v=737c4b720e609086
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.
Docs