KV vs Durable Objects: Decision Guide for Cloudflare Workers
When building Cloudflare Workers that need persistent state, decide between KV and Durable Objects. This guide compares consistency, latency, storage limits, pricing, and shows how to implement a counter with KV and a WebSocket session with a Durable Object.
06 Sept 2025, 07:23 UTC

Decision Context
Cloudflare Workers often need to persist data across invocations. Two native storage options exist: Cloudflare KV and Durable Objects. Choosing the right one hinges on consistency requirements, latency tolerance, storage size, and cost. This guide helps you decide which is appropriate for a given scenario.
Key Constraints
- Consistency: KV is eventually consistent (propagation <2 s). Durable Objects provide strong consistency with per‑key serialization.
- Latency & Throughput: KV offers sub‑10 ms reads at high throughput. Durable Objects incur ~5–10 ms per request due to single‑threaded execution.
- Storage Limits: KV is unlimited (tens of GB). Durable Objects are capped at 128 MiB per instance.
- Cost Model: KV is billed per 1 M read/write operations (free tier included). Durable Objects charge per request and per instance.
- Use‑Case Fit: KV suits simple key/value lookups; Durable Objects excel at per‑user session state, WebSocket handling, or transactions requiring serialization.
Option Comparison Table
| Feature | Cloudflare KV | Durable Objects |
|---|---|---|
| Consistency Model | Eventual (<2 s) | Strong, per‑key serialization |
| Read Latency | Sub‑10 ms | 5–10 ms (single‑threaded) |
| Write Latency | Sub‑10 ms | 5–10 ms |
| Throughput | Millions ops/sec (global) | Limited by single instance |
| Storage per Key | Unlimited | 128 MiB per object |
| Cost | Per 1 M reads/writes (free tier) | Per request + instance cost |
| Typical Use‑Cases | Static content, caching, counters | Session state, WebSockets, transactional data |
Trade‑Offs
- Consistency vs. Performance: If a write must be immediately visible, Durable Objects are necessary. For most counters or counters that tolerate a <2 s lag, KV is sufficient.
- Scalability vs. Isolation: KV scales automatically, but you lose per‑key serialization. Durable Objects isolate state per key, preventing race conditions but can become a bottleneck under heavy load.
- Storage Size: For large blobs or logs, KV wins. For per‑user session data that stays under 128 MiB, Durable Objects are fine.
- Cost: KV is cheaper for bulk reads/writes. Durable Objects cost more per request, so use them sparingly.
Concrete Implementation: KV Counter
Below is a minimal Worker that increments a global counter stored in KV. The counter is read, incremented, and written back in a single request. Because KV is eventually consistent, this pattern may double‑count under extreme concurrency; for production, consider using KV.put with a metadata version or a separate atomic counter service.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const key = 'global-counter';
let value = await KV.get(key);
value = (value ? parseInt(value, 10) : 0) + 1;
await KV.put(key, value.toString());
return new Response(`Counter: ${value}`);
}
wrangler.toml binding:
kv_namespaces = [
{ binding = "KV", id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }
]
Concrete Implementation: Durable Object WebSocket Session
Durable Objects are ideal for maintaining a WebSocket‑based chat session. The following example keeps a per‑user message count and echoes messages back to the client. The object serializes all accesses, guaranteeing that message counts are accurate even with concurrent messages.
export class ChatSession {
constructor(state, env) {
this.state = state;
}
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/connect') {
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
server.accept();
server.addEventListener('message', async event => {
const counter = (await this.state.storage.get('msgCount')) ?? 0;
await this.state.storage.put('msgCount', counter + 1);
server.send(`Echo: ${event.data}`);
});
return new Response(null, { status: 101, webSocket: client });
}
return new Response('Not Found', { status: 404 });
}
}
wrangler.toml for Durable Object:
durable_objects = [
{ name = "CHAT", class_name = "ChatSession", script_name = "chat" }
]
In the main Worker, retrieve the object instance per user ID:
export default {
async fetch(request, env) {
const userId = request.headers.get('cf-worker-id');
const obj = env.CHAT.get(userId);
return obj.fetch(request);
}
}
Validation Checklist
- Deploy the KV worker and perform a write followed by a read after 1 s. Verify the returned value matches the written value.
- Deploy the Durable Object, open a WebSocket client, send messages, and confirm the echo includes the incremented counter. Reconnect and ensure the counter persists.
- In the Cloudflare dashboard, compare request counts and costs for equivalent KV and Durable Object workloads.
Limitations & Tips
- KV’s eventual consistency can lead to stale reads; design idempotent writes or use cache‑busting keys when immediate visibility is required.
- Durable Objects are single‑threaded; heavy compute should be offloaded to Workers or external services to avoid blocking.
- The 128 MiB per‑object limit means you cannot store large sessions in a single Durable Object; consider sharding or external storage if needed.
- Always monitor request latency; if KV read latency spikes, consider using a local cache or a Cloudflare Cache API fallback.
Summary
Use Cloudflare KV for high‑throughput, low‑cost, eventually consistent key/value storage. Opt for Durable Objects when you need strong consistency, per‑key serialization, or persistent state across WebSocket connections. The table and code examples above provide a quick reference to guide your decision and help you implement the chosen approach.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.