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:

  1. Channel Resolution: The method resolves the ActorChannelFactory (either global or namespace-specific) to obtain an ActorChannel.
  2. Cache Initialization: The ActorChannel owns an ActorCacheInterface (concretely implemented as ActorCache). 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).
  3. Stub Binding: The DurableObject stub 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() invokes ActorCache::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::put at line 158 in actor-cache.c++) and schedules an asynchronous flush via the OutputGate.
  • Alarms: setAlarm() stores the alarm timestamp as a special DirtyAlarm entry, 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::ActorStorage service 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 = true permits parallel RPC calls
  • noCache = true prevents 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::get
  • put(key, value, options) → ActorCache::put
  • list(options) → Range queries with cursor support
  • delete(key) and deleteAll() → Removal operations
  • setAlarm(scheduledTime) and deleteAlarm() → 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:

  1. On creation, it captures a snapshot of the current cache state
  2. All writes are buffered in a transaction-local map
  3. commit() calls ActorCache::Transaction::commit(), which atomically applies buffered entries to the main cache and underlying storage
  4. 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) through DurableObjectNamespace::newUniqueId() or deterministic idFromName(), implemented in src/workerd/io/actor-id.h and src/workerd/api/actor.h.

  • Actor activation creates an isolated execution context via ActorChannel and ActorCache, initialized in DurableObjectNamespace::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 in src/workerd/api/actor-state.h) translates JavaScript KV calls into ActorCache methods, using an LRU for hot data and OutputGate for durable writes to SQLite or remote RPC.

  • Transactions provide atomicity through DurableObjectTransaction, which buffers writes and commits them via ActorCache::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 set allowConcurrency and noCache options, 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →