Cluster
Most of Effect lets you build a single process. Cluster lets you take stateful services and spread them across many machines — without changing the way you write your business logic. You define an entity (a sharded, addressable actor), give it an RPC protocol, and the cluster routes every message for a given id to exactly one live instance somewhere in the fleet. State lives in memory while the entity is active, and is recreated on demand after idle passivation or failover.
The cluster modules are unstable and import from effect/unstable/cluster. The
transport and storage wiring lives in the platform packages — for Node you assemble
a cluster with NodeClusterSocket.layer from @effect/platform-node.
import { Effect, Ref, Schema } from "effect"import { Entity } from "effect/unstable/cluster"import { Rpc } from "effect/unstable/rpc"
// 1. Describe the messages the entity can handle, as RPCs.const Increment = Rpc.make("Increment", { payload: { amount: Schema.Number }, success: Schema.Number})
// 2. Pair a stable entity-type name with its RPC protocol.const Counter = Entity.make("Counter", [Increment])
// 3. Implement the handlers. State (here a Ref) lives for as long as the// entity instance is active on its owning runner.const CounterLayer = Counter.toLayer( Effect.gen(function*() { const count = yield* Ref.make(0) return Counter.of({ Increment: ({ payload }) => Ref.updateAndGet(count, (n) => n + payload.amount) }) }))
// 4. From anywhere in the cluster, get a client for a specific entity id and// send it a message. Routing to the live instance is handled for you.const program = Effect.gen(function*() { const clientFor = yield* Counter.client const total = yield* clientFor("counter-123").Increment({ amount: 1 }) yield* Effect.log(`counter-123 is now ${total}`)})Why entities?
Section titled “Why entities?”An entity is the cluster-facing version of a stateful service. Two properties make it different from a plain service:
- Single owner per id. For any entity id (
"counter-123"), the cluster guarantees that messages are processed by a single live instance at a time, on whichever runner currently owns that id’s shard. You get serialized access to in-memory state without locks. - Location transparency. Callers never address machines. They ask for a client by entity id and send typed RPCs; the cluster handles routing, failover, and passivation (stopping idle entities and recreating them on the next message).
Because the protocol is described with RPC and Schema, every message is type-safe end to end and can be serialized for the wire or persisted to durable storage.
In this section
Section titled “In this section”- Entities — Define an entity’s RPCs, implement handlers that keep in-memory state, send messages from a client, and wire everything into a runnable cluster (including an in-memory test setup).
Related modules
Section titled “Related modules”The effect/unstable/cluster namespace also ships pieces you will reach for as your
deployment grows:
Singleton— register an effect that should have exactly one active owner across the whole cluster (a scheduler, a consumer, a polling loop). Ownership follows shard placement, so it fails over automatically.ClusterCron— run cron-scheduled work as a clustered singleton.Sharding/ShardingConfig— the lower-level service that computes shard ids, acquires shard locks, and drives placement and rebalancing.MessageStorage/SqlMessageStorage— durable mailbox storage for persisted messages, backed by SQL.
Cluster builds directly on RPC, Schema, Services & Layers, and the Platform packages — read those first if any of the snippets here are unfamiliar.