Using Cloudflare Workers Durable Objects for Consistent Per‑User Counters
Learn how Cloudflare Workers Durable Objects give you per‑user state with strong consistency, see a concrete counter example, and understand the latency, cost, and sharding trade‑offs.
23 Jun 2026, 16:44 UTC

Problem: Keeping per‑user state consistent without an external database
When you need to track something like a request counter, a lock, or a session flag for each user, reaching for a separate KV store or database adds latency, operational overhead, and the risk of race conditions if multiple Workers try to update the same key at the same time.
Thesis: Durable Objects give you a single, globally unique instance per key that guarantees sequential access and strong consistency, all inside the same Workers isolate.
How Durable Objects work
A Durable Object is a JavaScript class that Cloudflare instantiates on demand. Each instance is addressed by a unique ID derived from a key you choose (for example, a user ID or a room name). All HTTP requests that target the same ID are routed to the same object instance, and Cloudflare processes those requests one at a time. This eliminates race conditions without needing external locking mechanisms.
Inside the class you can:
- Use the built‑in Workers KV API for durable storage.
- Upgrade connections to WebSockets for real‑time communication.
- Set alarms to run code at a future time.
Worked example: a per‑user request counter
Below is a minimal Worker that creates a Durable Object binding, increments a counter for the supplied user ID, and returns the current value.
// worker.js
import { DurableObject } from 'cloudflare:workers'
class Counter extends DurableObject {
constructor(state, env) {
super(state, env)
// ensure the storage key exists
this.state.storage.put('count', 0)
}
async fetch(request) {
// increment and read back the count atomically
const current = await this.state.storage.get('count', { type: 'number' })
await this.state.storage.put('count', current + 1)
const newVal = await this.state.storage.get('count', { type: 'number' })
return new Response(String(newVal))
}
}
export default {
async fetch(request, env, ctx) {
// extract a user identifier from the request (e.g., header or query)
const url = new URL(request.url)
const userId = url.searchParams.get('uid') || 'anonymous'
// derive a deterministic ID from the userId
const id = env.COUNTER.idFromName(userId)
const object = env.COUNTER.get(id)
return object.fetch(request)
}
}
To make this work you need a Wrangler configuration that declares the Durable Object binding:
# wrangler.toml
name = "counter-worker"
main = "src/worker.js"
compatibility_date = "2024-09-01"
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
Trade‑offs and limitations
- Latency: Each request may involve a round‑trip to the Durable Object’s location, which can add a few milliseconds compared to a pure stateless Worker.
- Cost: You pay for compute time and storage writes; frequent updates to the same object can increase costs.
- Hot‑spot risk: If many users share the same ID (or you create too many unique IDs without sharding), a single Durable Object can become a bottleneck. Sharding by a hash of the user ID or using a composite key (e.g.,
${userId}#${date}) spreads the load.
Actionable closing
Start by running the example locally with wrangler dev. Send a few requests with different uid query parameters and observe that each user’s counter increments independently and monotonically. In the Cloudflare dashboard, inspect the Durable Object storage to confirm the count value persists across reloads. Monitor the cpu_time metrics to ensure latency stays within your budget, and adjust your sharding strategy if you notice a single object handling a disproportionate share of traffic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.