Using Cloudflare Durable Objects for a Simple Counter – Limits, Code, and Pitfalls
Learn how to build a stateful counter with Cloudflare Workers Durable Objects, see the exact code, understand size and concurrency limits, and avoid common pitfalls.
02 Aug 2026, 00:55 UTC

Why a Durable Object Counter?
When you need a counter that lives on the edge, stays in sync across all Cloudflare nodes, and doesn’t require a separate database, Durable Objects are the built‑in solution. They give you per‑key, stateful logic that is automatically replicated, with a tiny footprint and a straightforward API.
How Durable Objects Work
A Durable Object is a tiny class that runs inside a Worker. Each instance is identified by a unique key inside a namespace. When a request arrives, Cloudflare routes it to the edge location that owns that key, so all reads and writes hit the same place. The state is stored in a small, in‑memory cache that is flushed to KV storage on shutdown, ensuring persistence across restarts.
Key Concepts
- Namespace – the logical grouping of objects, defined in
wrangler.tomland referenced in the script. - Key – a string that identifies a particular instance. The same key always resolves to the same object.
- State – a
DurableObjectStateinstance passed to the constructor, providingstorageandfetchhelpers. - Concurrency – each object can service up to ~1,000 concurrent requests before Cloudflare starts throttling.
- Size – the total serialized state cannot exceed 64 KB.
- Quota – a namespace can contain up to 10,000 objects by default.
Concrete Example – Counter Worker
Below is a minimal, deployable example that increments a counter stored in a Durable Object. The counter persists across restarts and is safe for concurrent increments.
1. wrangler.toml
name = "counter-worker"
type = "javascript"
account_id = "<YOUR_ACCOUNT_ID>"
workers_dev = true
route = "" # optional
[durable_objects]
name = "COUNTER_NS"
class_name = "Counter"
2. worker.js
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request, event));
});
async function handleRequest(request, event) {
const url = new URL(request.url);
const path = url.pathname;
// Resolve the Durable Object for the counter key "global"
const counterDO = event.env.COUNTER_NS.get(request.headers.get("cf‑ray") ?? "global");
if (path === "/count") {
// POST increments, GET reads
if (request.method === "POST") {
return await counterDO.fetch(request);
} else if (request.method === "GET") {
const state = await counterDO.fetch(request);
return state;
}
}
return new Response("Not Found", { status: 404 });
}
// Durable Object class
export class Counter {
constructor(state, env) {
this.state = state;
this.env = env;
}
async fetch(request) {
// Use the object's internal storage to keep the counter
const current = (await this.state.storage.get("value")) ?? 0;
const newVal = current + 1;
await this.state.storage.put("value", newVal);
return new Response(`Counter: ${newVal}`);
}
}
3. Deploy and Test
- Run
wrangler publishto deploy. - Send a
POSTtohttps://your‑worker.workers.dev/count– you should seeCounter: 1, thenCounter: 2, etc. - After a few minutes, stop the worker (e.g., using
wrangler dev --stop), then restart and POST again – the counter should continue from where it left off, proving persistence.
Limits to Keep in Mind
| Limit | Value | Implication |
|---|---|---|
| Concurrent Requests | ~1,000 | Beyond this, Cloudflare may throttle or return 429. |
| State Size | 64 KB | Large objects will fail serialization; use KV or external storage for >64 KB. |
| Namespace Quota | 10,000 objects | Exceeding this requires a quota increase request. |
| Request Latency | ~10–30 ms | Edge routing adds negligible overhead; heavy logic can increase latency. |
Common Mistakes and How to Avoid Them
- Heavy computation in fetch – Keep
fetchlean. Offload CPU‑intensive work to a background task or separate Worker. - Ignoring serialization – All data stored via
state.storage.putmust be JSON‑serializable. Storing functions or circular references will throw. - Assuming global uniqueness – The key is scoped to the namespace. Two namespaces can have the same key without conflict.
- Storing binary blobs – The 64 KB limit is for serialized JSON. Binary data should be stored in KV or an external object store.
- Over‑using KV inside the same request – KV operations are asynchronous and can add latency. Cache frequently accessed values in
state.storagewhen possible.
Verifying Correctness
After deployment, monitor the following metrics in the Workers dashboard:
- Durable Object Requests – Should be close to the number of POSTs you send.
- 5xx Errors – A spike indicates hitting concurrency or size limits.
- Response bodies – Ensure they increment as expected after each POST.
To double‑check persistence, run:
# Send a POST to increment
curl -X POST https://your-worker.workers.dev/count
# Wait 5 minutes, then send again
curl -X POST https://your-worker.workers.dev/count
If the counter value increases across the pause, the state is correctly persisted.
When to Use Durable Objects
Durable Objects shine when:
- You need per‑entity, low‑latency state on the edge.
- The data fits within 64 KB and you rarely exceed 1,000 concurrent users per object.
- You want to avoid the overhead of a separate database and still benefit from Cloudflare’s global replication.
For larger datasets, heavy write loads, or global uniqueness requirements, consider KV, a managed database, or a combination of Durable Objects + KV.
Takeaway
Durable Objects provide a lightweight, replicated, per‑key state store that’s perfect for simple counters and similar patterns. By defining a minimal class, binding it in wrangler.toml, and using state.storage, you can build a fully stateful API that scales automatically. Remember the 64 KB size, 1,000 concurrency, and 10,000‑object quota limits, and avoid common pitfalls like heavy computation or non‑serializable data. With these guidelines, you can confidently add stateful logic to your Cloudflare Workers.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.