Skip to content

Actor

Actor

Overview

Actors are stateful compute units that maintain persistent data and handle requests with a unique identity. Each actor instance remembers its state between requests and can coordinate complex workflows over time.

Actors serve as persistent “mini-services” where each user session, shopping cart, or game room operates as its own isolated instance. When requests arrive, you route them to specific actors by ID, and those actors maintain all relevant state.

Key benefits include persistent state without external databases, unique identity for request routing, scheduled operations through built-in alarms, and complete isolation between instances.

Prerequisites

  • Active Raindrop project with manifest configuration
  • Understanding of TypeScript classes and async/await patterns
  • Familiarity with stateful vs stateless service architectures
  • Knowledge of actor model concepts and distributed systems

Creating/Getting Started

Define actors in your manifest file to enable stateful request handling:

application "user-sessions" {
actor "session-manager" {
visibility = "public"
domain {
fqdn = "sessions.example.com"
}
}
service "api" {
visibility = "public"
domain {
fqdn = "api.example.com"
}
}
}

Generate the actor implementation:

Terminal window
raindrop build generate

This creates the base actor class structure:

export class SessionManager extends Actor<Env> {
async fetch(request: Request): Promise<Response> {
return new Response('fetch method not implemented for actors', { status: 501 });
}
}

Accessing/Basic Usage

Access actors through services that route requests to specific instances:

export default class extends Service<Env> {
async fetch(request: Request): Promise<Response> {
// Extract user ID from request
const userId = this.getUserId(request);
// Get specific actor instance
const actorId = this.env.SESSION_MANAGER.idFromName(userId);
const actor = this.env.SESSION_MANAGER.get(actorId);
// Call custom actor methods
return await actor.handleSession(request);
}
private getUserId(request: Request): string {
const url = new URL(request.url);
return url.searchParams.get('userId') || 'anonymous';
}
}

Create custom methods in your actor class:

export class SessionManager extends Actor<Env> {
async fetch(request: Request): Promise<Response> {
return new Response('fetch method not implemented for actors', { status: 501 });
}
async handleSession(request: Request): Promise<Response> {
// Access persistent state
const sessionData = await this.state.storage.get('session');
if (!sessionData) {
// Initialize new session
await this.state.storage.put('session', {
userId: this.state.id.name,
createdAt: Date.now(),
lastActivity: Date.now()
});
}
return new Response(JSON.stringify(sessionData));
}
}
## Core Concepts
Actors follow the actor model pattern where each instance maintains isolated state and processes requests sequentially. This architecture ensures data consistency without complex locking mechanisms.
**Actor Identity**: Each actor has a unique ID that determines routing. You can create predictable IDs using `idFromName(string)` or generate random IDs with `newUniqueId()`.
**State Persistence**: Actor state persists across requests and survives restarts. The storage API provides key-value operations with atomic guarantees within a single actor instance.
**Request Routing**: Services route requests to specific actor instances based on business logic (user ID, session token, etc.). Multiple requests to the same actor ID reach the same instance.
**Alarm System**: Actors can schedule future operations using alarms. When an alarm triggers, the actor's `alarm()` method executes automatically.
## Domain Configuration
Actors can be configured with custom domains for direct external access, similar to services. This allows clients to communicate directly with specific actor instances.
### Domain Block Structure
Define domains within a `domain` block in your actor configuration:
```hcl
actor "my-actor" {
visibility = "public"
domain {
fqdn = "sessions.mycompany.com" # Custom domain
}
}
# OR
actor "my-actor" {
visibility = "public"
domain {
cname = "my-sessions" # Auto-generated subdomain
}
}

Configuration Requirements:

  • Each domain block must specify either cname OR fqdn (but not both)
  • Domain access is automatically enabled when a domain block is present
  • fqdn: Fully qualified domain name (e.g., “sessions.example.com”)
  • cname: Subdomain prefix that generates “<cname>.<org-id>.lmapp.run

Domain Validation Rules

Actor domain configurations follow the same validation rules as services:

FQDN Validation:

  • Must be a valid domain name format
  • Maximum length: 63 characters
  • Pattern: /^[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,6}$/

CNAME Validation:

  • Must be a valid subdomain prefix
  • Maximum length: ~35 characters (63 minus platform suffix)
  • Pattern validates as subdomain format

Actor Domain Examples

application "stateful-app" {
# User session actor with custom domain
actor "user-session" {
visibility = "public"
domain {
fqdn = "sessions.myapp.com"
}
}
# Game room actor with auto-generated subdomain
actor "game-room" {
visibility = "public"
domain {
cname = "game-rooms"
}
# Results in: game-rooms.org_abc123.lmapp.run
}
# Private actor accessible only through bindings
actor "data-processor" {
# No domain = internal access only
}
}

Storage Management

Actor storage provides persistent key-value operations that survive restarts and maintain data consistency across requests.

get(key)

Retrieves values by key with support for batch operations and type safety:

// Get single value with type safety
const userName = await this.state.storage.get<string>("user:name");
// Get multiple values atomically
const values = await this.state.storage.get<UserData>(["user:123", "user:456"]);
// Handle missing values
const data = await this.state.storage.get("session") ?? defaultSessionData;

Parameters:

  • key: string | string[] - Single key or array for batch retrieval
  • Generic type T - Expected value type for type safety

Returns: T | undefined for single keys, Map<string, T> for multiple keys

put(key, value)

Stores values with atomic guarantees for single actor instance operations:

// Store single value
await this.state.storage.put("user:123", {
name: "John Doe",
lastLogin: Date.now()
});
// Batch storage operation
await this.state.storage.put({
"session:abc": sessionData,
"preferences:123": userPrefs,
"cache:latest": cacheData
});

Parameters:

  • key: string | Record<string, any> - Key or object with key-value pairs
  • value: any - Value to store (omit for batch operations)

list(options?)

Lists stored keys with filtering and pagination support:

// List all data
const allData = await this.state.storage.list();
// Filter by prefix with pagination
const userSessions = await this.state.storage.list({
prefix: "session:",
limit: 50,
start: "session:abc123"
});
// Reverse chronological order
const recentActivity = await this.state.storage.list({
prefix: "activity:",
reverse: true,
limit: 10
});

Options:

  • start: string - Start from this key (inclusive)
  • startAfter: string - Start after this key (exclusive)
  • end: string - End before this key (exclusive)
  • prefix: string - Filter keys by prefix
  • reverse: boolean - List in reverse order
  • limit: number - Maximum keys to return

Returns: Map<string, T> of matching key-value pairs

delete(key)

Removes keys with confirmation of deletion count:

// Delete single key (returns boolean)
const wasDeleted = await this.state.storage.delete("temp:data");
// Delete multiple keys (returns count)
const deletedCount = await this.state.storage.delete([
"session:expired1",
"session:expired2"
]);

Parameters:

  • key: string | string[] - Key or array of keys to delete

Returns: boolean for single keys, number (count) for multiple keys

Alarm Scheduling

Schedule future operations that trigger automatically through the actor’s alarm() method.

getAlarm()

Check the currently scheduled alarm time:

const alarmTime = await this.state.storage.getAlarm();
if (alarmTime) {
const timeRemaining = alarmTime - Date.now();
console.log(`Alarm in ${timeRemaining}ms`);
}

Returns: number | null - Unix timestamp or null if no alarm set

setAlarm(scheduledTime)

Schedule an alarm for automatic execution:

// Schedule 30 minutes from now
const thirtyMinutes = Date.now() + (30 * 60 * 1000);
await this.state.storage.setAlarm(thirtyMinutes);
// Schedule using Date object
await this.state.storage.setAlarm(
new Date('2024-12-31T23:59:59Z').getTime()
);

Parameters:

  • scheduledTime: number | Date - Unix timestamp or Date object

deleteAlarm()

Cancel the scheduled alarm:

await this.state.storage.deleteAlarm();

Implement the alarm handler in your actor class:

export class SessionManager extends Actor<Env> {
async alarm(): Promise<void> {
// Clean up expired sessions
const sessions = await this.state.storage.list({ prefix: "session:" });
const now = Date.now();
for (const [key, session] of sessions) {
if (session.expiresAt < now) {
await this.state.storage.delete(key);
}
}
// Schedule next cleanup
await this.state.storage.setAlarm(now + (60 * 60 * 1000)); // 1 hour
}
}

Actor Lifecycle

Control actor execution timing and concurrency for initialization and cleanup operations.

waitUntil(promise)

Register promises that must complete before the actor can exit or be garbage collected:

export class DataProcessor extends Actor<Env> {
async processLargeFile(request: Request): Promise<Response> {
const fileUrl = await request.json();
// Process file in background, ensure completion before actor exits
const processingPromise = this.processFileAsync(fileUrl);
this.state.waitUntil(processingPromise);
return new Response('Processing started');
}
private async processFileAsync(url: string): Promise<void> {
// Heavy processing that should complete even if HTTP response sent
const data = await fetch(url).then(r => r.json());
await this.state.storage.put('processed-data', data);
}
}

Parameters:

  • promise: Promise<any> - Promise that must complete before actor exits

blockConcurrencyWhile(callback)

Block concurrent requests while executing initialization or critical sections:

export class SessionManager extends Actor<Env> {
private initialized = false;
constructor(state: ActorState, env: Env) {
super(state, env);
// Block all incoming requests until initialization completes
this.state.blockConcurrencyWhile(async () => {
if (!this.initialized) {
await this.loadFromStorage();
this.initialized = true;
}
});
}
private async loadFromStorage(): Promise<void> {
const config = await this.state.storage.get('config');
if (config) {
// Initialize actor state from stored configuration
}
}
}

Parameters:

  • callback: () => Promise<T> - Function to execute while blocking concurrency

Returns: Promise<T> - Result of the callback function

Actor Identity

Access the unique identifier that routes requests to this specific actor instance.

state.id

Get information about the current actor instance:

// Get string representation for logging
const actorId = this.state.id.toString();
console.log(`Processing in actor: ${actorId}`);
// Compare actor instances
const isSameActor = this.state.id.equals(otherActorId);
// Access name if created with idFromName()
const userName = this.state.id.name; // e.g., "user-123"

Properties:

  • toString(): string - String representation for logging/debugging
  • equals(other): boolean - Compare with another ActorId
  • name: string | undefined - Human-readable name if set

Actor Namespace Operations

Create and route to actor instances using the namespace interface available through environment bindings.

idFromName(name)

Generate deterministic actor IDs from string names:

export default class extends Service<Env> {
async fetch(request: Request): Promise<Response> {
const userId = this.extractUserId(request);
// Same name always produces same ID
const actorId = this.env.SESSION_MANAGER.idFromName(userId);
const actor = this.env.SESSION_MANAGER.get(actorId);
return await actor.handleRequest(request);
}
}

Parameters:

  • name: string - Name to generate ID from

Returns: ActorId - Deterministic actor identifier

get(id, options?)

Get or create an actor instance by ID:

// Basic actor access
const actor = this.env.MY_ACTOR.get(actorId);
// With location hint for performance
const actor = this.env.MY_ACTOR.get(actorId, {
locationHint: 'wnam' // West North America
});

Parameters:

  • id: ActorId - Target actor identifier
  • options.locationHint?: ActorLocationHint - Preferred geographic location

Location Hints:

  • wnam - West North America
  • enam - East North America
  • sam - South America
  • weur - West Europe
  • eeur - East Europe
  • apac - Asia Pacific
  • oc - Oceania
  • afr - Africa
  • me - Middle East

Returns: ActorStub<T> - RPC interface for making calls to the actor

jurisdiction(jurisdiction)

Create a namespace scoped to a specific legal jurisdiction:

// Access actors within EU jurisdiction
const euNamespace = this.env.USER_ACTOR.jurisdiction('eu');
const euActorId = euNamespace.idFromName('user-123');
const euActor = euNamespace.get(euActorId);
// Access actors within FedRAMP jurisdiction
const fedrampNamespace = this.env.USER_ACTOR.jurisdiction('fedramp');

Parameters:

  • jurisdiction: 'eu' | 'fedramp' - Legal jurisdiction for data residency

Returns: ActorNamespace<T> - Namespace scoped to the jurisdiction

Code Examples

Complete implementations demonstrating actor patterns and common use cases.

User Session Management

// manifest.hcl
application "user-app" {
actor "session-manager" {}
service "api" {
domain = "api.example.com"
}
}
// Actor implementation
export class SessionManager extends Actor<Env> {
async fetch(request: Request): Promise<Response> {
return new Response('Use custom methods only', { status: 501 });
}
async createSession(userId: string, userData: any): Promise<string> {
const sessionId = crypto.randomUUID();
const session = {
id: sessionId,
userId,
userData,
createdAt: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
};
await this.state.storage.put(`session:${sessionId}`, session);
// Set cleanup alarm
await this.state.storage.setAlarm(session.expiresAt);
return sessionId;
}
async getSession(sessionId: string): Promise<any | null> {
const session = await this.state.storage.get(`session:${sessionId}`);
if (!session || session.expiresAt < Date.now()) {
await this.state.storage.delete(`session:${sessionId}`);
return null;
}
return session;
}
async updateActivity(sessionId: string): Promise<void> {
const session = await this.state.storage.get(`session:${sessionId}`);
if (session) {
session.lastActivity = Date.now();
await this.state.storage.put(`session:${sessionId}`, session);
}
}
async alarm(): Promise<void> {
// Clean up expired sessions
const sessions = await this.state.storage.list({ prefix: "session:" });
const now = Date.now();
const expiredKeys = [];
for (const [key, session] of sessions) {
if (session.expiresAt < now) {
expiredKeys.push(key);
}
}
if (expiredKeys.length > 0) {
await this.state.storage.delete(expiredKeys);
}
// Schedule next cleanup
await this.state.storage.setAlarm(now + (60 * 60 * 1000));
}
}
// Service routing to actors
export default class extends Service<Env> {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const userId = this.extractUserId(request);
// Route to user-specific actor
const actorId = this.env.SESSION_MANAGER.idFromName(userId);
const actor = this.env.SESSION_MANAGER.get(actorId);
if (url.pathname === '/session/create') {
const userData = await request.json();
const sessionId = await actor.createSession(userId, userData);
return Response.json({ sessionId });
}
if (url.pathname === '/session/get') {
const sessionId = url.searchParams.get('sessionId');
const session = await actor.getSession(sessionId);
return Response.json({ session });
}
return new Response('Not found', { status: 404 });
}
private extractUserId(request: Request): string {
const authHeader = request.headers.get('Authorization');
// Extract from JWT or session token
return authHeader?.replace('Bearer ', '') || 'anonymous';
}
}

Shopping Cart Actor

export class ShoppingCart extends Actor<Env> {
async fetch(request: Request): Promise<Response> {
return new Response('Use custom methods only', { status: 501 });
}
async addItem(productId: string, quantity: number, price: number): Promise<void> {
const cart = await this.getCart();
const itemKey = `item:${productId}`;
const existingItem = cart.get(itemKey);
if (existingItem) {
existingItem.quantity += quantity;
existingItem.totalPrice = existingItem.quantity * existingItem.price;
} else {
cart.set(itemKey, {
productId,
quantity,
price,
totalPrice: quantity * price,
addedAt: Date.now()
});
}
await this.saveCart(cart);
await this.setExpirationAlarm();
}
async removeItem(productId: string): Promise<void> {
const cart = await this.getCart();
cart.delete(`item:${productId}`);
await this.saveCart(cart);
}
async getCartSummary(): Promise<any> {
const cart = await this.getCart();
const items = Array.from(cart.values());
const totalAmount = items.reduce((sum, item) => sum + item.totalPrice, 0);
return {
items,
totalAmount,
itemCount: items.length,
cartId: this.state.id.name
};
}
private async getCart(): Promise<Map<string, any>> {
const items = await this.state.storage.list({ prefix: "item:" });
return items;
}
private async saveCart(cart: Map<string, any>): Promise<void> {
const updates: Record<string, any> = {};
for (const [key, value] of cart) {
updates[key] = value;
}
await this.state.storage.put(updates);
}
private async setExpirationAlarm(): Promise<void> {
// Cart expires in 2 hours
const expirationTime = Date.now() + (2 * 60 * 60 * 1000);
await this.state.storage.setAlarm(expirationTime);
}
async alarm(): Promise<void> {
// Clear expired cart
const items = await this.state.storage.list({ prefix: "item:" });
const itemKeys = Array.from(items.keys());
if (itemKeys.length > 0) {
await this.state.storage.delete(itemKeys);
}
}
}
## raindrop.manifest
Configure actors in your manifest file to enable stateful request handling:
```hcl
application "session-app" {
actor "session-manager" {
visibility = "public"
domain {
fqdn = "sessions.myapp.com"
}
# Optional: Specify jurisdiction for data residency
jurisdiction = "eu" # or "fedramp"
}
actor "shopping-cart" {
visibility = "public"
domain {
cname = "shopping-carts"
}
# Actors with domains can handle direct requests
}
actor "data-processor" {
visibility = "application"
# Internal actor accessible only through bindings
}
service "api" {
visibility = "public"
domain {
fqdn = "api.example.com"
}
# Service can route requests to actors or handle directly
}
}