Agents SDK foundations
Create one agent instance per user or session
Build stateful agent boundaries on Durable Objects and route stable names without creating one global agent.
Workers are stateless by design. An agent that remembers a conversation needs somewhere for that memory to live. And that somewhere should not be one global object shared by every user.
The Cloudflare Agents SDK builds on Durable Objects. Each named agent instance gets its own persistent SQLite database and its own real-time WebSocket connections. Ask for the same name twice and you reach the same object with the same storage. Ask for a different name and you get a completely separate instance.
The configuration is a Durable Object binding plus a SQLite migration in wrangler.jsonc:
{
"durable_objects": {
"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }]
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }]
}
The new_sqlite_classes migration is what gives each instance its embedded SQLite database. Forget it and state calls fail at runtime, with an error that doesn’t point back at the config.
Pick the boundary first
The name you route to is the boundary. Name instances by user ID and each user gets isolated memory. Name them by chat session and one user can hold several independent conversations. Name them by workspace and a whole team shares one. Decide this before you write the first line of agent code, because changing it later means migrating state.
Route requests to an instance with routeAgentRequest, or address one directly by name:
import { routeAgentRequest, getAgentByName } from 'agents'
export default {
async fetch(request, env) {
const session = await authenticate(request)
if (!session) return new Response('unauthorized', { status: 401 })
return (await routeAgentRequest(request, env))
?? new Response('not found', { status: 404 })
},
}
The mistake to avoid is one global agent for everyone. A single instance runs all users’ work through one object, one request at a time. And every user’s data sits in the same SQLite database, one prompt injection away from another user’s conversation.
Names are routing, not security
Authenticate before you accept a connection, because the instance name is not a secret. If instance names are user IDs and you skip the auth check, anyone who guesses user-42 talks to that user’s agent. Authorization decides who may reach a name. The name only decides where the request goes.
Create two counter-agent instances and prove state persists and stays isolated. Increment counter-a three times and counter-b once, wait a minute, then read both again. You should get 3 and 1. Anything else and your boundary is imaginary.
Lesson completed