Skip to content

Durable Objects

For precise, globally consistent rate limiting or circuit breaking, Stoma provides a Durable Objects adapter. Each unique rate limit key maps to a Durable Object instance that maintains an atomic counter, ensuring exact counts across all Worker isolates worldwide.

Use Durable Objects instead of KV when you need:

  • Exact counting — billing, quota enforcement, or compliance scenarios where approximate counts are not acceptable
  • Strong consistency — every request must see the true current count, not an eventually consistent snapshot
  • Circuit breaking — a single DO instance per upstream can track failure counts and circuit state (closed/open/half-open) with guaranteed consistency
  • Session affinity — consistent routing for stateful connections

For high-volume rate limiting where approximate counts are acceptable, the KV approach is simpler and cheaper.

Stoma provides two exports from @homegrower-club/stoma/adapters:

  • RateLimiterDO — A Durable Object class that maintains an atomic rate limit counter with alarm-based expiry
  • DurableObjectRateLimitStore — A RateLimitStore implementation that communicates with RateLimiterDO instances

The RateLimiterDO class must be exported from your Worker entry point so the Cloudflare runtime can instantiate it:

// worker.ts (your entry point)
import { createGateway, rateLimit } from "@homegrower-club/stoma";
import { DurableObjectRateLimitStore } from "@homegrower-club/stoma/adapters";
// Re-export the DO class for the runtime
export { RateLimiterDO } from "@homegrower-club/stoma/adapters";
export default {
fetch(request: Request, env: Env) {
const store = new DurableObjectRateLimitStore(env.RATE_LIMITER);
const gateway = createGateway({
routes: [
{
path: "/api/*",
pipeline: {
policies: [rateLimit({ max: 100, store })],
upstream: { type: "url", target: "https://backend.internal" },
},
},
],
});
return gateway.app.fetch(request, env);
},
};

Declare the Durable Object binding and class:

[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiterDO"
[[migrations]]
tag = "v1"
new_classes = ["RateLimiterDO"]

The name field ("RATE_LIMITER") is the binding you pass to DurableObjectRateLimitStore via env.RATE_LIMITER. The class_name must match the exported class name.

The DurableObjectRateLimitStore maps each rate limit key to a unique DO instance via namespace.idFromName(key). When increment() is called:

  1. The store calls namespace.get(id) to obtain a stub for the DO instance
  2. It sends a fetch() request to the stub with the window duration as a query parameter
  3. The RateLimiterDO reads its counter from durable storage
  4. If the window is still active, the count is incremented atomically
  5. If the window has expired, a new counter starts at 1 and an alarm is scheduled to clean up the expired entry
  6. The updated count and reset timestamp are returned as JSON

Because each key maps to exactly one DO instance, and Durable Objects provide single-threaded execution guarantees, the counter is always accurate regardless of how many Worker isolates are handling concurrent requests.

The cloudflareAdapter factory automatically selects Durable Objects when a DO namespace is provided (preferred over KV):

import { createGateway, rateLimit } from "@homegrower-club/stoma";
import { cloudflareAdapter } from "@homegrower-club/stoma/adapters";
const adapter = cloudflareAdapter({
rateLimitDo: env.RATE_LIMITER,
});
const gateway = createGateway({
routes: [
{
path: "/api/*",
pipeline: {
policies: [
rateLimit({ max: 100, store: adapter.rateLimitStore }),
],
upstream: { type: "url", target: "https://backend.internal" },
},
},
],
});

If both rateLimitDo and rateLimitKv are provided, the adapter prefers Durable Objects.

The circuit breaker pattern benefits from strong consistency. A DO per upstream can track failure counts and circuit state transitions across all Worker isolates. While Stoma’s built-in InMemoryCircuitBreakerStore works for single-instance deployments, a DO-backed store ensures that circuit state is globally consistent.

CharacteristicDurable ObjectsKV
ConsistencyStrong (single-writer)Eventually consistent
AccuracyExact countsApproximate
Latency~10-50ms per operationSub-millisecond reads
CostHigher (per-request + storage)Lower (KV pricing)
SetupRequires DO class export + migrationKV namespace only
Use caseBilling, quotas, complianceAbuse prevention, general rate limiting