# How workerd Manages Durable Object Storage and Actor Lifecycle

> Discover how workerd manages Durable Object storage and actor lifecycle using a two-tier system with LRU cache and durable SQLite or RPC storage for seamless state management.

- Repository: [Cloudflare/workerd](https://github.com/cloudflare/workerd)
- Tags: internals
- Published: 2026-03-18

---

**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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/src/workerd/io/actor-id.h) and [`src/workerd/api/actor.h`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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`](https://github.com/cloudflare/workerd/blob/main/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.