Durable Objects are the closest you’ll get to running stateful, single-instance server logic directly within Cloudflare’s edge network.
Let’s see one in action. Imagine a simple counter that increments every time a user hits an endpoint.
// wrangler.toml
name = "durable-counter"
main = "src/index.js"
compatibility_date = "2023-10-25"
[[services]]
binding = "COUNTER"
service = "durable-counter"
[[routes]]
pattern = "/counter"
zone_name = "your-domain.com"
zone_id = "YOUR_ZONE_ID" # Get this from your Cloudflare dashboard
# src/index.js
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === "/counter") {
const id = env.COUNTER.idFromName("global-counter"); // Create a stable ID
const stub = env.COUNTER.get(id);
return stub.fetch(request);
}
return new Response("Not found", { status: 404 });
},
};
// src/counter.js (this is the Durable Object itself)
export class Counter {
constructor(state, env) {
this.state = state;
this.env = env;
// If the Durable Object is just being initialized, load the count from storage.
// Otherwise, the count is already in memory from a previous invocation.
this.state.storage.get("count").then(count => {
this.count = count || 0;
});
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/increment") {
this.count++;
// Persist the count to durable storage.
await this.state.storage.put("count", this.count);
return new Response(`Count is now: ${this.count}`, {
headers: { "Content-Type": "text/plain" },
});
} else if (url.pathname === "/get") {
return new Response(`Current count: ${this.count}`, {
headers: { "Content-Type": "text/plain" },
});
}
return new Response("Unknown path", { status: 404 });
}
}
When you deploy this and send a POST request to /counter/increment (or /counter/get to see the value), you’re not just hitting a random server. You’re hitting the exact same instance of the Counter Durable Object every time, regardless of which Cloudflare data center you originate from. The env.COUNTER.idFromName("global-counter") is the magic sauce here; it deterministically generates a unique ID for our object based on the string "global-counter." This ensures that all requests for this "global-counter" will be routed to the same object instance.
The core problem Durable Objects solve is managing state in a globally distributed, ephemeral edge environment. Traditional serverless functions are stateless by design; their execution is a discrete event, and any state must be externalized to a database or cache. Durable Objects bring state to the edge, co-located with the compute that needs it.
Internally, a Durable Object is a single JavaScript class instance. When a request arrives for a specific object ID, Cloudflare routes it to the data center currently hosting that object. If the object doesn’t exist yet, Cloudflare spins it up in one of its data centers and then routes the request. Subsequent requests for that same ID are always directed to that same instance. The state object provided to the constructor gives you access to state.storage, which is a durable key-value store that persists even if the object is shut down and restarted. This persistence is crucial. When the object is active, it keeps its state in memory (this.count in the example) for fast access, and state.storage acts as its persistent backing store.
The most surprising thing about Durable Objects is that they don’t have explicit "start" or "stop" lifecycle hooks in the traditional sense. Instead, their lifecycle is managed implicitly by Cloudflare based on incoming requests and internal resource management. An object instance is kept "warm" as long as it’s receiving requests or has pending operations. If it becomes idle for a period, Cloudflare can shut it down. When a new request arrives for that ID, Cloudflare will then instantiate the object again, loading its state from state.storage. This means your object’s constructor will be called on cold starts, and you must ensure it correctly hydrates its internal state from state.storage every time.
The state.blockConcurrencyWhile(async () => { ... }) method is your best friend for ensuring atomic operations. You wrap any code that modifies state or performs critical I/O within this block. While the code inside blockConcurrencyWhile is executing, Cloudflare will queue any incoming requests for that object instance, preventing race conditions where multiple requests might try to update the same piece of state simultaneously. This is how you guarantee that your increment operation, for instance, is truly atomic across concurrent requests.
The next challenge you’ll likely encounter is managing multiple Durable Objects, each with its own state, and coordinating between them.