Using Cloudflare Workers Durable Objects for Per‑User Rate Limiting
Learn how to use Cloudflare Workers Durable Objects to implement low‑latency, per‑user rate limits without an external database.
28 Oct 2025, 12:33 UTC

Problem: Keeping per‑user limits without a central store
When you expose a public API through Cloudflare Workers, you often need to enforce a request quota per API key or user ID. Reaching for an external KV namespace or a database adds latency and extra cost, especially when the limit is simple (e.g., \"no more than 5 requests per second\").
Thesis: Durable Objects give you a lightweight, strongly‑consistent bucket for each identifier
A Durable Object is a uniquely addressable, single‑threaded instance that holds its own storage and processes requests sequentially. By binding a Durable Object to a Worker and routing each request to the object named after the user ID, you get automatic isolation and transactional reads/writes without managing a separate database.
How it works
- The Worker script declares a Durable Object binding, e.g.,
const COUNTER: DurableObjectNamespace;. - On each request, extract the identifier (API key, JWT sub, etc.) and call
COUNTER.get(id)to get the object’s stub. - The Durable Object class defines
fetch(or a WebSocket handler) that increments a counter stored inthis.storage, checks the limit, and returns an appropriate response. - Because each object runs in its own isolate, two different IDs never share state, and the single‑threaded nature guarantees that increments and reads are atomic.
Worked example: per‑API‑key rate limiter
// src/worker.js
export default {
async fetch(request, env, ctx) {
const { pathname, searchParams } = new URL(request.url);
const apiKey = searchParams.get('key');
if (!apiKey) {
return new Response('Missing API key', { status: 400 });
}
const id = env.RATE_LIMITER.idFromString(apiKey);
const stub = env.RATE_LIMITER.get(id);
return await stub.fetch(request);
},
};
// src/durable_object.js
export class RateLimiter {
constructor(state, env) {
this.state = state;
this.limit = 5;
this.windowMs = 10_000;
}
async fetch(request) {
const now = Date.now();
const storage = await this.state.storage.get();
if (!storage) {
await this.state.storage.put({ count: 1, start: now });
return new Response('OK', { status: 200 });
}
const { count, start } = storage;
const elapsed = now - start;
if (elapsed > this.windowMs) {
await this.state.storage.put({ count: 1, start: now });
return new Response('OK', { status: 200 });
}
if (count >= this.limit) {
return new Response('Rate limit exceeded', { status: 429 });
}
await this.state.storage.put({ count: count + 1, start });
return new Response('OK', { status: 200 });
}
}
To try it locally:
- Install Wrangler (
npm i -D wrangler). - Create a project (
wrangler init rate-limiter-worker) and add the binding inwrangler.toml:
[[durable_objects.bindings]]
name = \"RATE_LIMITER\"
class_name = \"RateLimiter\"
- Run
wrangler devand hithttp://127.0.0.1:8787/?key=user123several times. - You should see a 200 response for the first five requests, then a 429 until the 10‑second window resets.
- Open another tab with a different key (
?key=user456) and verify that its counter is independent.
After confirming locally, deploy with wrangler publish and repeat the same checks against the production URL.
Trade‑off and limitation
- Each Durable Object processes requests sequentially; a heavy computation inside
fetchwill block other requests for the same ID. For high‑throughput keys, consider sharding (e.g., hash the key into multiple object IDs) or offloading work to a regular Worker. - Default storage is a few megabytes; the rate‑limiter example stays well under that, but if you need to keep larger per‑user state (e.g., session histories), you should spill over to KV or R2 and keep only a summary in the Durable Object.
Actionable closing
If you need low‑latency, per‑identifier coordination — whether it’s a counter, a game‑state lock, or a simple quota — start by defining a Durable Object binding, route requests by the identifier, and keep the object’s logic lightweight. Test locally with wrangler dev, verify independent behavior with different IDs, then deploy and monitor the Durable Object metrics in the Cloudflare dashboard (request count, storage usage, latency) to ensure you stay within the object’s limits.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.