WebSockets
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.
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 |
onStart() is called once when the agent first starts, before any connections are established:
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 }}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 }}Define onConnect and onMessage methods on your Agent to accept WebSocket connections:
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 })); } }}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 })); } }}Each connected client has a unique Connection object:
| Property/Method | Type | Description |
|---|---|---|
id | string | Unique identifier for this connection |
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 |
Store data specific to each connection (user info, preferences, etc.):
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}`); }}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}`); }}Use this.broadcast() to send a message to all connected clients:
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 })); }}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 })); }}Pass an array of connection IDs to exclude from the broadcast:
// Broadcast to everyone except the senderthis.broadcast( JSON.stringify({ type: "user-typing", userId: "123" }), [connection.id], // Do not send to the originator);// Broadcast to everyone except the senderthis.broadcast( JSON.stringify({ type: "user-typing", userId: "123" }), [connection.id], // Do not send to the originator);Tag connections for easy filtering. Override getConnectionTags() to assign tags when a connection is established:
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); } }}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); } }}| 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 |
Messages can be strings or binary (ArrayBuffer / ArrayBufferView):
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); // ... } }}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); // ... } }}Handle connection errors and disconnections:
export class ChatAgent extends Agent { async onError(connection, error) { console.error(`Connection ${connection.id} error:`, error); // Clean up any resources for this connection }
async onClose(connection, code, reason, wasClean) { console.log(`Connection ${connection.id} closed: ${code} ${reason}`);
// Notify other clients this.broadcast( JSON.stringify({ event: "user-left", userId: connection.state?.userId, }), ); }}export class ChatAgent extends Agent { async onError(connection: Connection, error: unknown) { console.error(`Connection ${connection.id} error:`, error); // Clean up any resources for this connection }
async onClose( connection: Connection, code: number, reason: string, wasClean: boolean, ) { console.log(`Connection ${connection.id} closed: ${code} ${reason}`);
// Notify other clients this.broadcast( JSON.stringify({ event: "user-left", userId: connection.state?.userId, }), ); }}| Type | Description |
|---|---|
string | Text message (typically JSON) |
ArrayBuffer | Binary data |
ArrayBufferView | Typed array view of binary data |
Agents support hibernation — they can sleep when inactive and wake when needed. This saves resources while maintaining WebSocket connections.
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 };}- 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
| 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++; }}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(), }), ); }}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), ); } }}For browser connections, use the Agents client SDK:
- Vanilla JS:
AgentClientfromagents/client - React:
useAgenthook fromagents/react
Refer to Client SDK for full documentation.