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.
When to use Durable Objects
Section titled “When to use Durable Objects”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 expiryDurableObjectRateLimitStore— ARateLimitStoreimplementation that communicates withRateLimiterDOinstances
1. Export the Durable Object class
Section titled “1. Export the Durable Object class”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 runtimeexport { 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); },};2. Configure wrangler.toml
Section titled “2. Configure wrangler.toml”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.
How it works
Section titled “How it works”The DurableObjectRateLimitStore maps each rate limit key to a unique DO
instance via namespace.idFromName(key). When increment() is called:
- The store calls
namespace.get(id)to obtain a stub for the DO instance - It sends a
fetch()request to the stub with the window duration as a query parameter - The
RateLimiterDOreads its counter from durable storage - If the window is still active, the count is incremented atomically
- If the window has expired, a new counter starts at 1 and an alarm is scheduled to clean up the expired entry
- 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.
Using the cloudflareAdapter factory
Section titled “Using the cloudflareAdapter factory”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.
Circuit breaking with Durable Objects
Section titled “Circuit breaking with 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.
Trade-offs
Section titled “Trade-offs”| Characteristic | Durable Objects | KV |
|---|---|---|
| Consistency | Strong (single-writer) | Eventually consistent |
| Accuracy | Exact counts | Approximate |
| Latency | ~10-50ms per operation | Sub-millisecond reads |
| Cost | Higher (per-request + storage) | Lower (KV pricing) |
| Setup | Requires DO class export + migration | KV namespace only |
| Use case | Billing, quotas, compliance | Abuse prevention, general rate limiting |