A Durable Object (DO) is a globally-unique stateful instance
addressed by name: every request for "user-123" — from any Worker,
anywhere in the world — lands on the same instance, with its own
transactional SQLite-backed storage. That combination of identity +
storage + single-threaded execution makes DOs the right tool for
per-entity state: counters, chat rooms, game sessions, WebSocket
hubs, rate limiters, collaborative documents.
In alchemy a DO is a class with the same two-phase Effect pattern as
a Worker, and every method you return becomes
a typed RPC method — no schemas, no serialization boilerplate. This
page builds one from scratch: a Counter that keeps a per-key count
in transactional storage, exposes RPC methods, and streams a
sequence of numbers back to the caller.
Like Workers, a DO has two Effect.gen blocks: the outer one
resolves the instance’s state and any shared dependencies, and
the inner one returns the public API.
Create src/counter.ts with the smallest possible DO — empty
public API, no state yet:
Provides a way to write effectful code using generator functions, simplifying
control flow and error handling.
When to use
Use when you want to write effectful code that looks and behaves like
synchronous code, while still handling asynchronous tasks, errors, and complex
control flow such as loops and conditions.
Generator functions work similarly to async/await but keep errors,
requirements, and interruption in the Effect type. You can yield* values
from effects and return the final result at the end.
Provides a way to write effectful code using generator functions, simplifying
control flow and error handling.
When to use
Use when you want to write effectful code that looks and behaves like
synchronous code, while still handling asynchronous tasks, errors, and complex
control flow such as loops and conditions.
Generator functions work similarly to async/await but keep errors,
requirements, and interruption in the Effect type. You can yield* values
from effects and return the final result at the end.
Each DO instance has its own key/value storage, backed by SQLite.
Resolve Cloudflare.DurableObjectState in the outer Effect,
then pull the current count out of storage in the inner init so it
survives restarts and hibernation:
Effect.gen(function* () {
conststate=yield*Cloudflare.DurableObjectState;
returnEffect.gen(function* () {
let count = (yield*state.storage.get<number>("count")) ??0;
return {};
});
});
Cloudflare.DurableObjectState is the same per-instance handle
Cloudflare exposes for storage, setAlarm, acceptWebSocket,
and friends. We’ll use it more in the next part for WebSockets.
Any function you return from the inner Effect that produces an
Effect becomes a typed RPC method. Add one to mutate the count
and one to read it:
Effect.gen(function* () {
conststate=yield*Cloudflare.DurableObjectState;
returnEffect.gen(function* () {
let count = (yield*state.storage.get<number>("count")) ??0;
return {};
return {
increment: () =>
Effect.gen(function* () {
count +=1;
yield*state.storage.put("count", count);
return count;
}),
get: () =>Effect.succeed(count),
};
});
});
Workers will see counter.increment() returning
Effect<number> and counter.get() returning Effect<number>,
fully type-checked through the Cloudflare RPC machinery.
returnHttpServerResponse.text("Hello from my Worker!");
}),
};
}),
);
counters.getByName(name) returns a typed stub: the Worker sees
increment(): Effect<number> and get(): Effect<number> exactly as
you defined them. Calling .increment() round-trips through
Cloudflare’s RPC machinery to the durable instance.
RPC isn’t limited to single values — any Effect (or Stream) you
return becomes a typed RPC method. Let’s add a tick(n) method that
emits a sequence of numbers 100ms apart, then expose an HTTP route
that streams them back to the client.
The DO produces values lazily, the runtime ferries each chunk back
to the Worker, and the Worker pipes them straight onto the HTTP
response — all type-checked end-to-end.
Everything you just built is RPC — and you never declared a
schema. Any function returning an Effect (or a Stream) from the
inner block becomes a typed method, and getByName(name) hands
callers a stub that mirrors those signatures exactly:
This schemaless, typed-stub pattern — the DO shape of
Schemaless RPC — is the recommended way to talk
to a Durable Object. The types flow from the implementation — add a
method, and every caller sees it immediately; change a return type,
and every caller that mismatches fails to compile. The same
mechanism works across Workers: one Worker can host the DO while
others bind to it by class — see
Bind to another Worker’s Durable Object.
Alchemy also ships a schema-based RpcDurableObject that serves an
Effect RPC group on the DO’s fetch. It is no longer the suggested
path for DO method calls — the schemaless bridge covers typed
methods and streaming without the ceremony. Its one remaining niche
is when return values are Schema.Class instances whose identity
must survive the boundary — the schemaless bridge serializes return
values and strips class identity — see
Cloudflare.RpcDurableObject.
If you want schema-validated procedures (payload validation,
tagged error schemas shared with external clients), reach for
Effect RPC on a Worker instead.