How workerd Manages Durable Object Storage and Actor Lifecycle
workerd implements Durable Objects as stateful actors backed by a two-tier storage system that combines an in-memory LRU cache with durable SQLite or remote RPC storage, coordinating lifecycle transitions through unique ID generation, actor channel activation, and transactional state access.
Durable Objects provide the stateful compute primitive in the Cloudflare Workers runtime. In the cloudflare/workerd repository, the implementation spans identity management, storage caching, and actor lifecycle orchestration. This article examines how workerd allocates unique actor identities, manages the transition from dormant to active states, and persists data through the ActorCache architecture.
Durable Object Identity and ID Generation
Every Durable Object instance is identified by a unique DurableObjectId. The ID is a 128-bit value encoded as a 64-character hexadecimal string, generated through cryptographically secure randomness or deterministic hashing.
In src/workerd/api/actor.h, the DurableObjectNamespace class provides two primary factory methods:
newUniqueId()(line 84): Generates a random 128-bit ID, ensuring global uniqueness across the Workers platform.idFromName(): Hashes a provided name string deterministically using SHA-256, producing the same ID for identical inputs.
The DurableObjectId class defined in src/workerd/io/actor-id.h stores the raw bytes and optional jurisdiction constraints (region restrictions). When DurableObjectNamespace::get() or getByName() is invoked, the system wraps the ID in a DurableObject stub that JavaScript can use to send requests to the actor.
Actor Lifecycle Phases
The Durable Object lifecycle transitions through three distinct phases: identity allocation, activation/channel creation, and state access. Each phase is managed by specific components in the workerd codebase.
Phase 1: Creation and ID Allocation
When a Worker script calls newUniqueId() or idFromName(), the runtime executes the logic in src/workerd/api/actor.h. The resulting DurableObjectId is wrapped in a jsg::Ref and returned to JavaScript. At this stage, no storage resources are allocated; the ID exists purely as a reference.
Phase 2: Actor Activation and Channel Creation
Activation occurs when the runtime receives a request targeting a specific Durable Object ID. In src/workerd/api/actor.h, the getImpl() method (line 92) orchestrates this transition:
- Channel Resolution: The method resolves the
ActorChannelFactory(either global or namespace-specific) to obtain anActorChannel. - Cache Initialization: The
ActorChannelowns anActorCacheInterface(concretely implemented asActorCache). If no cache exists for this ID, a new instance is created with a connection to the underlying storage RPC client (rpc::ActorStorage::Stage::Client). - Stub Binding: The
DurableObjectstub returned to JavaScript holds a reference to this channel, enabling method calls to reach the activated actor.
Phase 3: State Access and Storage Operations
Once activated, the Durable Object executes JavaScript code that interacts with storage through the DurableObjectStorage API. In src/workerd/api/actor-state.h, the DurableObjectStorageOperations mixin implements methods like get(), put(), list(), delete(), and setAlarm().
Each operation translates to corresponding methods on the ActorCache:
- Reads:
storage.get()invokesActorCache::get(), which first checks the LRU cache. If the key is absent, it issues an RPC call to the storage service. - Writes:
storage.put()creates a dirty entry in the cache (ActorCache::putat line 158 inactor-cache.c++) and schedules an asynchronous flush via theOutputGate. - Alarms:
setAlarm()stores the alarm timestamp as a specialDirtyAlarmentry, flushed through the same gate mechanism.
The ActorCache Storage Architecture
The ActorCache class in src/workerd/io/actor-cache.c++ provides the concrete implementation of the storage interface. It serves as a write-back cache with specific consistency and durability guarantees.
Memory Management and LRU
The cache maintains an LRU (Least Recently Used) list of entries. Each entry tracks its state:
- CLEAN: Data matches the underlying storage
- DIRTY: Data has been modified but not yet flushed
- PENDING: A read request is in flight
The SharedLru structure allows memory pressure to evict clean entries while preserving dirty entries until they are persisted.
Persistence and OutputGate
Writes are not immediately durable. Instead, they pass through the OutputGate, a synchronization primitive that batches storage operations and applies back-pressure. When the cache size exceeds a soft limit, the gate triggers a flush to:
- SQLite: An in-process database file for local development or single-node deployments
- Remote RPC: The
rpc::ActorStorageservice for distributed production environments
The recordStorageWrite() method ensures that JavaScript execution waits for the gate to open if the cache is under back-pressure, preventing memory exhaustion.
Direct I/O Bypass
For specific use cases requiring strong consistency or large scans, the storage API supports direct I/O. When useDirectIo() returns true (as implemented by SyncKvStorage), operations bypass the LRU cache entirely:
allowConcurrency = truepermits parallel RPC callsnoCache = trueprevents result caching
This mode is configured in DurableObjectStorageOperations::configureOptions (lines 71-77 in actor-state.h).
JavaScript API Implementation
The JavaScript-facing storage API is implemented through a combination of mix-ins and specialized transaction classes.
DurableObjectStorageOperations
Defined in src/workerd/api/actor-state.h, this mixin class provides the standard KV interface:
get(key, options)→ActorCache::getput(key, value, options)→ActorCache::putlist(options)→ Range queries with cursor supportdelete(key)anddeleteAll()→ Removal operationssetAlarm(scheduledTime)anddeleteAlarm()→ Alarm scheduling
Each method converts JavaScript options (like allowConcurrency or noCache) into C++ ReadOptions or WriteOptions structures through inline conversion operators defined in the header.
Transaction Support
DurableObjectTransaction extends DurableObjectStorageOperations to provide atomicity:
- On creation, it captures a snapshot of the current cache state
- All writes are buffered in a transaction-local map
commit()callsActorCache::Transaction::commit(), which atomically applies buffered entries to the main cache and underlying storage- Uncommitted transactions are discarded on destruction, providing automatic rollback
This implementation ensures that multi-key updates are atomic relative to other actor operations, though the actor itself processes requests serially (single-threaded execution model).
Key Source Files and Implementation Details
The Durable Object storage system spans multiple directories in the workerd codebase:
| File | Role |
|---|---|
src/workerd/api/actor.h |
Public API for DurableObjectNamespace and stub creation; contains newUniqueId() (line 84) and getImpl() (line 92) |
src/workerd/api/actor-state.h |
Mix-in implementing DurableObjectStorageOperations with KV methods and transaction support |
src/workerd/api/actor.c++ |
Bridge creating ActorChannel objects and connecting namespaces to the cache layer |
src/workerd/io/actor-id.h |
Definition of DurableObjectId (64-digit hex, 128-bit storage) and jurisdiction handling |
src/workerd/io/actor-id-impl.c++ |
ID parsing, encoding, and deterministic name hashing |
src/workerd/io/actor-cache.h |
Abstract interface ActorCacheInterface and ActorCacheOps definitions |
src/workerd/io/actor-cache.c++ |
Concrete ActorCache implementation with LRU logic, dirty entry tracking, and OutputGate integration; contains put implementation (line 158) |
src/workerd/io/actor-storage.capnp.h |
Cap'n Proto schema for RPC communication with remote storage services |
Summary
-
workerd assigns each Durable Object a unique 128-bit ID (
DurableObjectId) throughDurableObjectNamespace::newUniqueId()or deterministicidFromName(), implemented insrc/workerd/io/actor-id.handsrc/workerd/api/actor.h. -
Actor activation creates an isolated execution context via
ActorChannelandActorCache, initialized inDurableObjectNamespace::getImpl()(line 92), establishing the connection between the JavaScript stub and the storage layer. -
Storage operations flow through a two-tier cache where
DurableObjectStorageOperations(defined insrc/workerd/api/actor-state.h) translates JavaScript KV calls intoActorCachemethods, using an LRU for hot data andOutputGatefor durable writes to SQLite or remote RPC. -
Transactions provide atomicity through
DurableObjectTransaction, which buffers writes and commits them viaActorCache::Transaction::commit(), ensuring multi-key updates are applied atomically relative to other actor operations. -
Direct I/O bypass is available for specialized access patterns, configured via
useDirectIo()to setallowConcurrencyandnoCacheoptions, forcing immediate RPC calls without LRU caching.
Frequently Asked Questions
How does workerd generate unique Durable Object IDs?
workerd generates unique IDs through the DurableObjectNamespace::newUniqueId() method in src/workerd/api/actor.h (line 84). This method creates a cryptographically secure random 128-bit value and encodes it as a 64-character hexadecimal string. For deterministic ID generation, idFromName() hashes the provided name using SHA-256 to produce consistent IDs across invocations. The DurableObjectId class defined in src/workerd/io/actor-id.h stores these values along with optional jurisdiction constraints for region-specific placement.
What happens when a Durable Object is activated for the first time?
Activation occurs when a request targets a specific Durable Object ID through DurableObjectNamespace::get() or getByName(). The getImpl() method (line 92 in src/workerd/api/actor.h) resolves an ActorChannelFactory to create or reuse an ActorChannel. This channel initializes an ActorCacheInterface (concretely ActorCache) if none exists, establishing a connection to the underlying storage RPC client. The JavaScript stub returned to the user holds a reference to this channel, enabling method calls to reach the activated actor instance.
How does the ActorCache handle data consistency and durability?
The ActorCache in src/workerd/io/actor-cache.c++ implements a write-back caching strategy with specific consistency guarantees. Reads check an in-memory LRU cache first, falling back to RPC calls if data is absent. Writes create dirty entries immediately visible to the local actor but flush asynchronously through the OutputGate, which batches operations and applies back-pressure when memory limits are exceeded. The cache ensures that all writes are eventually persisted to either the local SQLite database or remote storage service, while maintaining strong consistency for reads within a single actor instance due to the single-threaded execution model.
What is the difference between standard storage operations and direct I/O in Durable Objects?
Standard storage operations flow through the ActorCache LRU, providing low-latency access to frequently used data with automatic batching of writes. Direct I/O bypasses this cache entirely when useDirectIo() returns true (as implemented by specialized storage classes). In direct mode, operations set allowConcurrency = true and noCache = true in DurableObjectStorageOperations::configureOptions (lines 71-77 in src/workerd/api/actor-state.h), forcing immediate RPC calls without caching results. This mode is useful for large scans or operations requiring strong consistency with external storage states, though it incurs higher latency per operation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →