Almost every guide to agent memory is a guide to which store to bolt on — a vector database, Mem0, Letta, or Zep, a SQLite file vs a service. All of them assume the same shape: the agent is one thing, its memory is another thing somewhere else, and you wire them together.
Cloudflare's Agents SDK quietly offers a different shape. Because each agent instance is a Durable Object — a single-threaded stateful micro-server with compute and its own SQLite database attached — memory doesn't live somewhere else. It lives inside the agent, on the same machine that runs its logic. There's nothing to provision. This walkthrough takes you from an empty Worker to an agent that remembers across sessions, and flags the one thing this shape can't do.
The mental model: two tiers of memory in one object#
A Cloudflare agent has two places to put memory, and picking the right one is the whole skill:
- Fast state —
this.setState()/this.state. A JSON blob that persists across requests and across hibernation, and auto-syncs to any connected client over WebSocket. This is your working set: the current task, user preferences, a short rolling history. - Durable SQL — the per-agent SQLite database, reached through the
this.sqltagged template. This is the long tail you don't want to hold in memory every turn: the full message log, events, anything you'll filter or aggregate.
Small and always-needed goes in state. Large or queried-on-demand goes in SQL. That's the same core-vs-archival split MemGPT made famous — except here both tiers live in the same object, not across a network.
1. Define the agent and its working state#
Install the SDK (npm install agents) and extend the Agent class. initialState seeds the fast tier:
import { Agent, callable } from "agents";
type MemoryState = {
displayName?: string;
recent: string[]; // last few turns, kept small on purpose
};
export class MemoryAgent extends Agent<Env, MemoryState> {
initialState: MemoryState = { recent: [] };
@callable()
setName(name: string) {
this.setState({ ...this.state, displayName: name }); // persists + syncs to clients
return this.state.displayName;
}
}
this.setState survives requests and hibernation, so the next time this user's agent wakes, this.state.displayName is still there — no load step, no cache to warm.
2. Create the durable table (once)#
The SQLite database ships with the object; you just declare your schema. Do it in onStart() so it's ready before any request touches it:
async onStart() {
this.sql`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
role TEXT NOT NULL,
content TEXT NOT NULL,
ts INTEGER NOT NULL
)`;
}
No connection string, no pool, no migration service. The IF NOT EXISTS makes it idempotent across cold starts.
3. Write and read the long-term log#
this.sql is a tagged template — interpolations are bound as parameters, not string-concatenated, so it's injection-safe by construction:
@callable()
remember(role: string, content: string) {
this.sql`
INSERT INTO messages (role, content, ts)
VALUES (${role}, ${content}, ${Date.now()})`;
// keep the fast tier small: mirror only the last 8 turns into state
const recent = [...this.state.recent, content].slice(-8);
this.setState({ ...this.state, recent });
}
@callable()
history(limit = 50) {
return this.sql`
SELECT role, content, ts FROM messages
ORDER BY ts DESC LIMIT ${limit}`; // returns an array of rows
}
The pattern that keeps agents fast: the working set lives in synced state and is always in hand; the full record lives in SQL and is fetched only when you need to look back. That directly avoids the failure mode where an agent's memory rots because everything is crammed into the context window.
4. Let the agent maintain itself#
Because the object has compute, it can act on its own memory on a schedule — prune old rows, summarize a long session into a compact note, expire stale facts:
@callable()
scheduleNightlyCompaction() {
this.schedule("0 3 * * *", "compact"); // cron: 03:00 daily
}
async compact() {
const old = this.sql`SELECT count(*) AS n FROM messages`;
// ...summarize + delete rows older than N days...
}
this.schedule supports one-time, recurring, and cron tasks, and they survive hibernation — the agent wakes to run them and goes back to sleep.
The one thing this shape can't do#
A Durable Object gives you exact recall — by key, id, timestamp, or a LIKE filter — and nothing more. There is no built-in vector search. The question every long-running agent eventually asks — "what did this user tell me about their deploy setup three weeks ago?" — is a semantic query, and SQL can't answer it by meaning.
So the honest framing is: **Durable Objects are an excellent place to keep an agent's memory and a poor place to search it by meaning without help.** When you need similarity recall, you add embeddings yourself — store vectors in Cloudflare Vectorize, or, for a tiny per-agent set, keep a brute-force cosine scan inside the object. If semantic recall is the primary access pattern across many users, that's exactly when a bolt-on layer like Mem0 or a shared vector service earns its keep instead.
When to reach for it#
Same rule as always with state: don't run a server until you have to. Durable Objects let you not run one at all.
- Reach for it when your agent is per-user or per-session, you want state and compute co-located at the edge, and your recall is mostly by key, id, or recency. You get isolation (one object per entity), zero infra, and hibernation that makes millions of idle agents free.
- Reach for something else when memory is a shared pool many agents query, when semantic retrieval is the product, or when you're not on Cloudflare's stack — that's Mem0/Letta/Zep or a real vector store territory.
One caveat before you build on it: the Agents SDK is a fast-moving 0.x. Pin your version, read the changelog before you upgrade, and expect the surface to shift month to month. The shape is right; the API isn't frozen yet.



