
Cloudflare Durable Objects (DOs) are single-instance, single-threaded actors with strongly-consistent storage. Each DO has a unique name, and all requests for that name route to the same instance — guaranteed by Cloudflare's runtime. When you need per-tenant state with strict consistency, DOs are unique among managed serverless primitives.
But they're easy to over-use. DOs are expensive per request, and most state doesn't need single-instance consistency. This guide covers the three patterns where DOs are clearly the right tool for webhook infrastructure, and where reaching for them is over-engineering.
What Durable Objects actually are
A DO is an actor. Send it a request, it processes one at a time, returns a response. Multiple requests to the same DO queue serially. Multiple requests to different DOs run in parallel. Each DO has access to its own storage (a SQLite-style key-value, isolated from every other DO).
The defining property: all state for a given name is in one place. There's no consistency model to reason about because there's only one instance.
This makes DOs unique. KV, D1, and R2 are eventually consistent across regions or transactional within a single database — neither model gives you "actor-style serialization for arbitrary state with arbitrary names." DOs do.
The three patterns where DOs win for webhook state
In Hooksbase, three pieces of state needed actor-style serialization. None of them are big surprises; they're all problems where the alternative would be either eventual consistency (wrong) or a centralized bottleneck (slow).
1. Per-webhook delivery sequencing
For webhooks configured in strict FIFO mode, the next event must wait until the previous succeeds (or terminally fails). This requires per-webhook serialization — but if you put it in a global database table, you create cross-webhook contention.
A DO per webhook gives you exactly the right scope: serialization within a webhook, parallelism across webhooks. The WebhookCoordinator DO holds the sequence number and the "is the head currently in flight" flag. New events go in; the head dispatches; the next becomes the head when the previous resolves.
This is impossible to do correctly with stateless functions and a shared database. You'd need optimistic locking with retry loops, or pessimistic locks that contend across webhooks. The DO model gives you single-writer semantics for free.
2. Per-project quota token buckets
Rate limits need atomic check-and-decrement semantics. "If the bucket has tokens, take one; otherwise reject." A naive implementation in a shared database has race conditions: two parallel requests both read 1 token remaining, both decrement, both proceed.
A DO per project holds the token bucket. Every quota check goes to the DO; the DO is single-threaded so the check-and-decrement is atomic by construction. The ProjectQuotaCoordinator DO does this for ingest rate limits.
You could do the same with Redis INCR or DynamoDB conditional writes. The trade-off is operational overhead and latency. For a Workers-native architecture, DOs are the local equivalent of those primitives without the cross-region call.
3. Replay dedup
When a customer (or a misconfigured automation) tries to replay the same delivery twice in parallel, you don't want to create two child deliveries. The dedup needs to be serialized per source delivery.
The ReplayCoordinator DO is named by the source delivery ID. The first replay request creates the child; the second sees the child already exists and returns a no-op. Without the DO, parallel requests could each see "no child exists" and both create one.
Where DOs are the wrong tool
DOs are expensive per-invocation compared to a stateless function reading from D1. Every request goes to a specific DO, which means a network hop and an actor startup. The overhead is fine when serialization actually matters — and overkill when it doesn't.
Don't use DOs for:
- Read-heavy queries that don't need actor consistency. Put it in D1 with a read replica or KV with a TTL.
- Append-only logs without serialization needs. Put it in D1 (or stream to R2 or Analytics Engine).
- Caching. KV is faster and cheaper.
- Large per-tenant data sets. The DO storage limit is real; if your per-tenant data grows unbounded, hybrid the DO with D1 or R2.
- Anything that needs cross-tenant queries. DOs are isolated by design.
The rule we landed on: reach for a DO when serialization is the problem. If two parallel requests would race against each other and you'd want one to wait, that's a DO. If they're independent reads and writes, that's D1 or KV.
DO sizing: per what?
The choice of name shapes what a DO does. Three common patterns in webhook infrastructure:
- Per-tenant (per project, per customer) — quota buckets, per-tenant rate limits, per-tenant secrets-rotation state.
- Per-resource (per webhook, per source delivery) — sequencing, replay dedup, per-resource leases.
- Per-shard (modulo on tenant or resource ID) — when the per-tenant or per-resource granularity would create too many DOs and the locality isn't needed.
Hooksbase uses per-resource for sequencing and replay (one DO per webhook, one per source delivery) and per-tenant for quotas (one DO per project). The total DO count grows with the number of webhooks and the active replay surface, which is bounded by tier and by recency.
DO storage vs application state
DO storage is a SQLite-style key-value (transactional, strongly consistent within the DO). Use it for the actor's own state — the in-memory state you're serializing access to.
For application state that survives the DO's lifetime or needs to be queryable from outside, write to D1 or R2 from inside the DO. The DO's job is to be the serialization point, not the system of record.
A common pattern: the DO holds a small in-memory state (current sequence number, current token count); on every state change, it writes through to D1 for durability and queryability. The DO storage holds the latest-known state for fast access; D1 is the canonical record.
Where to go next
- Building webhook infrastructure on Cloudflare Workers for the broader architecture
- Event infrastructure for AI agents for the agent-focused argument
- Routing, transforms, and replay for AI agents for the routing model that uses these primitives
- Deterministic replay for agents for the replay pattern the
ReplayCoordinatorenables