Diagnosing Cloudflare Workers Durable Objects: State, Performance, and Concurrency
When Durable Objects lose state, lag, or lose updates, the culprit is often a mis‑configured key, cold start, or race condition. This guide walks through common symptoms, ordered checks, quick fixes, and escalation steps.
16 Dec 2025, 17:19 UTC

Problem Statement
When a Durable Object (DO) behaves inconsistently—state not retained, first request latency spikes, or concurrent writes lose updates—it usually points to one of three categories:
- Persistence failure – the DO’s internal state or KV bindings aren’t working as expected.
- Cold‑start or performance bottleneck – the first request after deployment or a missing prefetch causes high latency.
- Concurrency issue – simultaneous writes race and overwrite each other.
This guide gives you a diagnostic flow to isolate the root cause, apply a fix, and know when to raise a support ticket.
Diagnostic Flow
- Verify DO key uniqueness
- Each request must target a unique DO ID. If you reuse the same key (e.g., hard‑coded string) across users, state will be shared unintentionally.
- Check your routing logic:
const id = env.OBJECTS.idFromName(request.url.split('/')[2]). - Use
wrangler tailto log the ID per request.
- Confirm KV binding presence (if used)
- In
wrangler.toml, ensure the KV namespace is bound:kv_namespaces = [{ binding = "COUNTER_KV", id = "" }]. - If the script references
env.COUNTER_KVbut the binding is missing, you’ll see a 500 error mentioning “KV namespace not found”.
- In
- Check for cold‑start latency
- Deploy the script with
wrangler deploy. - Send a request immediately and record the response time. Subsequent requests should be <5 ms.
- High latency (>200 ms) on the first request indicates a cold start; prefetch the DO in your application or enable
prefetch = truein the DO binding.
- Deploy the script with
- Test concurrent writes
- Spin up two Workers that send a PUT request to the same DO key simultaneously.
- After both finish, read the counter. If the value is
1instead of2, a race condition exists. - Use
state.lock()to serialize updates.
- Inspect request limits and throttling
- Check the
cloudflare.com/analyticsdashboard for 429 responses. - The per‑namespace KV write limit is ~10 000 writes/sec. Exceeding it triggers throttling.
- Per‑DO request limit is 1 000 requests/sec; exceeding it returns 429.
- Check the
Common Fixes
1. Ensure Unique DO Keys
Use idFromName or idFromString with a user‑specific identifier:
const id = env.COUNTER.idFromName(userId);
Don’t hard‑code the same ID for all users.
2. Declare KV Bindings Correctly
In wrangler.toml:
kv_namespaces = [
{ binding = "COUNTER_KV", id = "" }
]
And in the script:
const value = await env.COUNTER_KV.get(userId);
3. Reduce Cold‑Start Latency
Enable prefetch:
kv_namespaces = [
{ binding = "COUNTER_KV", id = "", prefetch = true }
]
Or make an initial request from your application to warm the DO.
4. Serialize Concurrent Updates
Wrap state changes in a lock:
async fetch(request) {
const state = this.state;
await state.lock(async () => {
let counter = await state.storage.get('counter') ?? 0;
counter++;
await state.storage.put('counter', counter);
});
return new Response(counter.toString());
}
Locks are per‑object, so this prevents lost updates when multiple requests hit the same DO key.
5. Handle Throttling Gracefully
Implement exponential back‑off for 429 responses:
async function safeWrite(kv, key, value) {
let attempts = 0;
while (true) {
try {
await kv.put(key, value);
break;
} catch (e) {
if (e.status === 429 && attempts < 5) {
await new Promise(r => setTimeout(r, 2 ** attempts * 100));
attempts++;
} else {
throw e;
}
}
}
}
Example Durable Object: Counter
Below is a minimal DO that increments a counter stored in its internal state. Use it to verify persistence and concurrency.
export class Counter {
async fetch(request) {
const state = this.state;
await state.lock(async () => {
let counter = await state.storage.get('counter') ?? 0;
counter++;
await state.storage.put('counter', counter);
});
const counter = await state.storage.get('counter');
return new Response(counter.toString());
}
}
export default {
async fetch(request, env) {
const id = env.COUNTER.idFromName(request.headers.get('X-User-Id') || 'default');
const stub = env.COUNTER.get(id);
return stub.fetch(request);
}
};
Deploy with:
wrangler deploy
Test persistence by sending 10 sequential requests from the same X-User-Id header and verify the counter increments to 10.
Escalation Criteria
- If all checks above pass but the DO still loses state, suspect a Cloudflare infrastructure issue.
- Gather logs via
wrangler tailand theWorkerdashboard. - Open a support ticket with Cloudflare, attaching the
wrangler.toml, relevant logs, and a reproduction script.
Limitations & Verification
- Durable Object state is capped at 128 KB. Exceeding it will silently fail writes. Verify by checking the size of stored data.
- KV write limits are per‑namespace. Use
wrangler kv:namespace:infoto monitor usage. - Billing is per request; excessive DO calls can inflate costs. Monitor the
Billingsection in the Cloudflare dashboard.
Practical Check
After applying a fix, run a quick sanity test:
curl -H "X-User-Id: alice" https://.workers.dev/counter
# Expect an incremented number each call.
If the value persists across restarts and increments correctly under concurrent load, the issue is resolved.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.