Building Your First Stateful Serverless Application with Cloudflare Durable Objects from Scratch
In the world of Serverless, we’ve grown accustomed to the stateless nature of cloud functions. Whether it’s Cloudflare Workers or AWS Lambda, they’re like fish with only seven seconds of memory - each request is a completely fresh start. This is highly efficient for scenarios like handling API requests or hosting static websites.
But a question arises: What if I need to maintain state between multiple requests?
For example, you might want to build a real-time online voting system, a simple web counter, or a collaborative document application. The traditional approach is to introduce external databases or caching services like Redis. This is certainly feasible, but it undoubtedly increases architectural complexity, network latency, and ultimately costs.
Is there a simpler way to give Serverless functions “memory”?
The answer is yes. Today, we’ll explore Cloudflare’s specialized tool for this purpose - Durable Objects (DO) - and walk you through building a globally distributed real-time counter to experience its power and elegance firsthand.
What are Durable Objects?
Simply put, Durable Objects are stateful Serverless.
You can think of them as a database that exists in the cloud with persistent storage. Unlike traditional Workers that are destroyed after a request ends, a Durable Object will continue to exist as long as it’s needed and safely preserves its own state.
To understand this more clearly, let’s look at the core differences between it and traditional Workers:
Feature | Cloudflare Worker (Traditional) | Cloudflare Durable Object |
---|---|---|
State | Stateless | Stateful |
Lifecycle | Destroyed after request ends | Continues to exist as long as needed |
Consistency | Eventual consistency | Strong consistency (within a single object) |
Concurrency Model | Massively parallel | Single-threaded (no concurrency conflicts within object) |
Ideal Use Cases | API gateways, static site hosting | Chat rooms, game state, shopping carts, real-time collaboration |
Core Working Mechanism
Behind the magic of Durable Objects are three key components:
- ID (
DurableObjectId
): Each DO instance has a unique “ID card”. Cloudflare ensures that all requests with the same ID are routed to the globally unique DO instance. - Stub: This is a “proxy” or “handle” that exists in regular Workers. Workers cannot directly communicate with DOs; instead, they use this Stub to locate and send messages to the corresponding DO instance.
- Single-threaded execution: This is one of the most elegant designs of DOs. For any DO instance, it will only process one request at a time. It’s like a bank counter with only one window - all requests need to queue for processing. This naturally solves the Race Condition problem, and you no longer need to worry about dirty data caused by concurrent writes.
Enough theory - let’s get hands-on!
Hands-on Practice: Building a Global Real-time Counter
We’ll use Cloudflare’s command-line tool wrangler
to create a project that implements an API for incrementing, decrementing, and reading counter values.
Prerequisites
First, make sure you have Node.js and npm installed. Then, we need to install wrangler
and log into your Cloudflare account.
# 1. Install Wrangler CLI
npm install -g wrangler
# 2. Log into your Cloudflare account
wrangler login
Step 1: Initialize the Project
Create a new Worker project.
wrangler init do-counter-app
cd do-counter-app
Step 2: Write the Durable Object Class
This is our core business logic. Create a new file src/counter.js
and write the following code:
// src/counter.js
// Define our Durable Object class
export class Counter {
constructor(state, env) {
this.state = state;
}
// Handle incoming requests
async fetch(request) {
// Parse URL to determine user intent
const url = new URL(request.url);
let value = (await this.state.storage.get("value")) || 0;
switch (url.pathname) {
case "/increment":
value++;
break;
case "/decrement":
value--;
break;
case "/":
// Do nothing, just read current value
break;
default:
return new Response("Not found", { status: 404 });
}
// Write the updated value back to persistent storage
await this.state.storage.put("value", value);
// Return current value
return new Response(value);
}
}
Code Explanation:
constructor(state, env)
: The constructor receives astate
object, wherestate.storage
is the API we use to read and write persistent data.fetch(request)
: This is the entry point for DO, just like a regular Worker. We determine what operation to perform by parsing the request’s URL path (like/increment
).this.state.storage.get("value")
: Asynchronously reads the key named “value” from the DO’s private storage. If it doesn’t exist, defaults to 0.this.state.storage.put("value", value)
: Asynchronously writes the new counter value back to storage. This operation is atomic and will be persisted.
Step 3: Configure wrangler.toml
This is the crucial step that connects our code to the Cloudflare platform. Open the wrangler.toml
file in the root directory and modify it as follows:
name = "do-counter-app"
main = "src/index.js"
compatibility_date = "2023-10-30" # Please use a more recent date
# Key part: Declare and bind Durable Object
[durable_objects]
bindings = [
{ name = "COUNTER", class_name = "Counter" }
]
[[migrations]]
tag = "v1" # Required, used to identify this binding
new_classes = ["Counter"] # Add our Counter class to the migration
Configuration Explanation:
- The
[durable_objects]
section declares the DO we want to use. name = "COUNTER"
: This is the binding name we use in Worker code to access this DO.class_name = "Counter"
: This points to the class name we exported insrc/counter.js
.[[migrations]]
: Every time you add or remove a DO class, you need to add a migration. This is how Cloudflare manages DO class changes.
Step 4: Write the Main Worker
Now, we need to write the entry Worker (src/index.js
), which will serve as the gateway for user requests and be responsible for forwarding requests to our Counter
DO.
Modify the src/index.js
file content as follows:
// src/index.js
// Import our Durable Object class so Wrangler can find it
export { Counter } from './counter.js';
export default {
async fetch(request, env) {
// We need a method to get a specific and unique DO instance.
// Here we use a fixed name "my-counter" to ensure all requests hit the same counter instance.
// `idFromName` will generate a unique ID based on the name.
let id = env.COUNTER.idFromName("my-counter");
// The `get` method uses this ID to get the DO's "Stub" (proxy).
let stub = env.COUNTER.get(id);
// Forward the user's original request directly to the obtained DO instance.
// The DO instance will execute the fetch method we wrote earlier.
return await stub.fetch(request);
},
};
Code Explanation:
export { Counter } from './counter.js';
: This line is important - it allowswrangler
to identify and package our DO class during deployment.env.COUNTER
: Theenv
object contains the bindings we defined inwrangler.toml
.COUNTER
is the name we defined.env.COUNTER.idFromName("my-counter")
: We use a fixed string “my-counter” to get a deterministic, unique DO ID. This means all requests using this name will be routed to the same DO instance.env.COUNTER.get(id)
: Get the DO’s Stub through the ID.stub.fetch(request)
: The Worker forwards the original request to the DO for processing and returns its response to the user.
Step 5: Deploy and Test
Everything is ready! Run the deployment command in your project root directory:
wrangler deploy
After successful deployment, wrangler
will give you a URL, such as https://do-counter-app.<your-subdomain>.workers.dev
.
Now, open your terminal and test it using curl
:
-
Increment the counter:
curl https://do-counter-app.<your-subdomain>.workers.dev/increment # Expected return: 1
-
Increment again:
curl https://do-counter-app.<your-subdomain>.workers.dev/increment # Expected return: 2
-
Get current value:
curl https://do-counter-app.<your-subdomain>.workers.dev/ # Expected return: 2
-
Decrement the counter:
curl https://do-counter-app.<your-subdomain>.workers.dev/decrement # Expected return: 1
You’ll find that no matter how many requests you send or how long the intervals are, the counter value is correctly “remembered”. You’ve just successfully built and deployed your first stateful Serverless application!
Conclusion and Outlook
Through this simple counter, we’ve learned the core idea of Durable Objects: binding state and computation together, with global access through a unique ID. It perfectly solves the state management challenges of Serverless while ensuring data consistency through the single-threaded model.
Now that you’ve mastered the basics, what else can you think of using it for?
- A simple online voting system (using different voting topics as parameters for
idFromName
). - A rate limiter that records API call counts.
- A simple WebSocket chat room (yes, Durable Objects have excellent support for WebSockets!).
Durable Objects open a new door for Serverless architecture. The next time you need to add “memory” to your Worker, I hope you’ll remember it.