Skip to content

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}`)
})

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.

A running cluster is a handful of cooperating modules. You usually only touch Entity and the platform layer that bundles the rest, but it helps to know what is underneath:

  • Entity (Entity) — the addressable actor and its RPC protocol. You define the messages, implement the handlers (holding in-memory state), and obtain a client by id. See Entities.
  • Sharding (Sharding, ShardingConfig) — the runtime service. It hashes entity ids into shard ids within a shard group, places those shards on a hash ring of healthy runners, and routes each message to the runner that currently owns the target shard. See Sharding & runners.
  • Runners & transport (SocketRunner, HttpRunner, SingleRunner, TestRunner) — the per-process node and the wire protocol between nodes. The platform packages bundle a transport: NodeClusterSocket.layer (raw TCP) and the HTTP/WebSocket runners for HTTP deployments. SingleRunner runs the whole cluster in one process; TestRunner runs it in memory for tests. See Sharding & runners.
  • Storage (MessageStorage, RunnerStorage) — where the cluster keeps its coordination state and (optionally) durable mailboxes. The SQL-backed layers (SqlMessageStorage, SqlRunnerStorage) give you durability across restarts; the in-memory layers are for development and tests. See Message storage.
  • Singleton & ClusterCron (Singleton, ClusterCron) — register work that should have exactly one owner across the whole fleet, with ownership following shard placement so it fails over automatically. See Singletons & cron.

The boundary that matters most is volatile vs persisted messaging: with the in-memory storage layers a message lives only as long as the process, while with the SQL-backed layers a message is written to durable storage before it is acknowledged, so it survives a runner crash and is redelivered on failover. That trade-off, and how to opt into persistence per message, is covered in Message storage.

  • 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).
  • Sharding & runners — How Sharding hashes ids into shards and places them on runners, the ShardingConfig knobs, and the available runner transports (SocketRunner, HttpRunner, SingleRunner, TestRunner, plus NodeClusterSocket).
  • Singletons & cron — Run an effect with a single fleet-wide owner using Singleton, and schedule clustered recurring work with ClusterCron.
  • Message storage — Volatile vs persisted messaging, MessageStorage / RunnerStorage, and the SQL-backed durable layers.
  • Entity proxies — Expose an entity over an external protocol (e.g. an HTTP/RPC server) with EntityProxy / EntityProxyServer.
  • Reference — The remaining modules at a glance: addresses and ids (EntityAddress, EntityId, RunnerAddress, ShardId, SingletonAddress), Envelope / Message / Reply, Snowflake, ClusterError, ClusterMetrics, and more.

Beyond the pieces above, the effect/unstable/cluster namespace also ships lower-level building blocks you reach for less often:

  • Snowflake — the time-ordered unique id generator used for message ids.
  • ClusterMetrics — predefined metrics for shard counts, message rates, and storage activity.
  • ClusterWorkflowEngine — runs the Workflow engine on top of the cluster so durable workflows execute across the fleet.

Cluster builds directly on RPC, Schema, Services & Layers, and the Platform packages — read those first if any of the snippets here are unfamiliar.