Cluster reference: ids, errors, metrics, workflows
This page is a scannable reference for the smaller cluster modules that don’t warrant their own page: the identifier types that name entities and runners, the snowflake ids the cluster uses for time-ordered request ids, the typed errors cluster operations fail with, the metrics the runtime emits, and the cluster-backed workflow engine that runs durable workflows on top of sharding. Authoring entities lives on the Entities page; this page covers the building blocks around them.
Identifiers and addresses
Section titled “Identifiers and addresses”Cluster routing identity is built from a few branded values. Each make brands
a trusted value without validation; each module also exports a Schema of the
same name for decoding/encoding at boundaries.
EntityId
Section titled “EntityId”A branded string naming one entity instance within an entity type. Routing hashes the exact string, so choose stable, deterministic values (not display names or emails).
import * as EntityId from "effect/unstable/cluster/EntityId"
const id = EntityId.make("user-42")// => "user-42" (branded EntityId)
// Decode/validate at a boundary with the schema:import { Schema } from "effect"Schema.decodeUnknownSync(EntityId.EntityId)("user-42")// => "user-42"EntityType
Section titled “EntityType”A branded string naming a kind of entity (the family), distinct from a
specific instance. Combined with an EntityId to address an entity.
import * as EntityType from "effect/unstable/cluster/EntityType"
const type = EntityType.make("User")// => "User" (branded EntityType)EntityAddress
Section titled “EntityAddress”A Schema.Class combining entityType, entityId, and shardId — the full
routing target for an entity. This is what you receive from
Entity.CurrentAddress inside an entity handler. Equality and hashing include
all three fields, and toString formats them.
import * as EntityAddress from "effect/unstable/cluster/EntityAddress"import * as EntityType from "effect/unstable/cluster/EntityType"import * as EntityId from "effect/unstable/cluster/EntityId"import * as ShardId from "effect/unstable/cluster/ShardId"
const address = EntityAddress.make({ entityType: EntityType.make("User"), entityId: EntityId.make("user-42"), shardId: ShardId.make("default", 7)})
address.entityId // => "user-42"String(address) // => "EntityAddress(User, user-42, default:7)"Inside an entity handler you read the current address from context rather than constructing it yourself:
import { Effect } from "effect"import * as Entity from "effect/unstable/cluster/Entity"
const handler = Effect.gen(function* () { const address = yield* Entity.CurrentAddress yield* Effect.log(`handling ${address.entityId}`)})MachineId
Section titled “MachineId”A branded Int identifying a runner. Its main job is to be the machine
component baked into snowflake ids; concurrent generators must
use distinct machine ids to stay unique.
import * as MachineId from "effect/unstable/cluster/MachineId"
const machine = MachineId.make(3)// => 3 (branded MachineId)Snowflakes encode the machine component in 10 bits, so only the value modulo 1024 is represented in an id.
Snowflake ids
Section titled “Snowflake ids”A Snowflake is a branded bigint: a runner-unique, time-ordered id packed
from a millisecond timestamp, a machine id, and a per-machine sequence number.
The custom epoch is 2025-01-01 UTC, the machine id occupies 10 bits, and the
sequence occupies 12 bits (4096 ids per millisecond per machine). The cluster
uses snowflakes for request/envelope ids; you mostly obtain them via
Sharding.getSnowflake, but the module is fully usable on
its own.
import * as Snowflake from "effect/unstable/cluster/Snowflake"import * as MachineId from "effect/unstable/cluster/MachineId"
// Pack parts into an id, then decompose it back:const id = Snowflake.make({ timestamp: Date.UTC(2025, 5, 1), machineId: MachineId.make(3), sequence: 0})Snowflake.toParts(id)// => { timestamp: 1748736000000, machineId: 3, sequence: 0 }Snowflake (constructor) and Snowflake (type)
Section titled “Snowflake (constructor) and Snowflake (type)”Snowflake(input) brands a bigint, or a bigint-compatible string, as a
Snowflake. The Snowflake type is Brand.Branded<bigint, TypeId>.
import * as Snowflake from "effect/unstable/cluster/Snowflake"
Snowflake.Snowflake(123n)// => 123n (branded Snowflake)Snowflake.Snowflake("123")// => 123n (branded Snowflake)Snowflake.make
Section titled “Snowflake.make”Packs known timestamp, machineId, and sequence parts into a branded id.
Machine id is encoded modulo 1024 and sequence modulo 4096 — out-of-range values
wrap rather than error.
import * as Snowflake from "effect/unstable/cluster/Snowflake"import * as MachineId from "effect/unstable/cluster/MachineId"
const id = Snowflake.make({ timestamp: Date.UTC(2025, 0, 1, 0, 0, 1), // 1s after the epoch machineId: MachineId.make(3), sequence: 5})// => a branded bigint encoding (timestamp, machineId=3, sequence=5)timestamp / dateTime / machineId / sequence / toParts
Section titled “timestamp / dateTime / machineId / sequence / toParts”Accessors that decode a snowflake back into its components. toParts returns all
three at once.
import * as Snowflake from "effect/unstable/cluster/Snowflake"import * as MachineId from "effect/unstable/cluster/MachineId"
const id = Snowflake.make({ timestamp: Date.UTC(2025, 0, 1, 0, 0, 1), machineId: MachineId.make(3), sequence: 5})
Snowflake.timestamp(id) // => 1735689601000 (Unix ms)Snowflake.dateTime(id) // => DateTime.Utc(2025-01-01T00:00:01.000Z)Snowflake.machineId(id) // => 3Snowflake.sequence(id) // => 5Snowflake.toParts(id)// => { timestamp: 1735689601000, machineId: 3, sequence: 5 }SnowflakeFromBigInt / SnowflakeFromString
Section titled “SnowflakeFromBigInt / SnowflakeFromString”Schemas for crossing transport/storage boundaries. SnowflakeFromBigInt brands
a bigint; SnowflakeFromString decodes from a string and encodes back to one
— show both directions.
import { Schema } from "effect"import * as Snowflake from "effect/unstable/cluster/Snowflake"
// bigint <-> branded bigintSchema.decodeSync(Snowflake.SnowflakeFromBigInt)(123n)// => 123n (branded)
// string -> branded bigint, and back to stringconst id = Schema.decodeSync(Snowflake.SnowflakeFromString)("123")// => 123n (branded)Schema.encodeSync(Snowflake.SnowflakeFromString)(id)// => "123"Generator / makeGenerator / layerGenerator
Section titled “Generator / makeGenerator / layerGenerator”Snowflake.Generator is a Context.Service wrapping a stateful generator with a
synchronous nextUnsafe() and an effectful setMachineId. makeGenerator
builds one (using Clock), and layerGenerator provides the service. The
generator starts with a random machine id, never moves time backward across
clock drift, resets the sequence each millisecond, and rolls into the next
millisecond if more than 4096 ids are requested.
import { Effect } from "effect"import * as Snowflake from "effect/unstable/cluster/Snowflake"import * as MachineId from "effect/unstable/cluster/MachineId"
const program = Effect.gen(function* () { const gen = yield* Snowflake.Generator yield* gen.setMachineId(MachineId.make(3)) const a = gen.nextUnsafe() const b = gen.nextUnsafe() // a < b (ids are monotonically increasing) return [a, b]}).pipe(Effect.provide(Snowflake.layerGenerator))constEpochMillis
Section titled “constEpochMillis”The custom snowflake epoch in Unix milliseconds.
import * as Snowflake from "effect/unstable/cluster/Snowflake"
Snowflake.constEpochMillis// => 1735689600000 (Date.UTC(2025, 0, 1))Cluster errors
Section titled “Cluster errors”Cluster operations fail with typed, schema-backed error classes so callers
can distinguish routing, membership, serialization, persistence, and mailbox
failures. Each has a stable _tag, so you catch them with Effect.catchTag /
Effect.catchTags, and most expose a static is guard.
Only a few of these surface from client calls — MailboxFull,
AlreadyProcessingMessage, and PersistenceError. The rest
(EntityNotAssignedToRunner, RunnerNotRegistered, RunnerUnavailable) arise
during internal routing and are typically retried by higher-level cluster logic
because ownership and health can change while a message is in flight.
EntityNotAssignedToRunner
Section titled “EntityNotAssignedToRunner”A runner received a message for an entity it does not own. Carries the offending
address. Usually transient during rebalancing and retried internally.
// _tag: "EntityNotAssignedToRunner", address: EntityAddressMalformedMessage
Section titled “MalformedMessage”A message failed at a schema encode/decode (serialization) boundary — not an
entity handler failure. Carries the underlying cause. MalformedMessage.refail
maps an effect’s failures into this error.
import { Effect } from "effect"import { MalformedMessage } from "effect/unstable/cluster/ClusterError"
const decoded = MalformedMessage.refail( Effect.fail("bad payload"))// fails with: new MalformedMessage({ cause: "bad payload" })PersistenceError
Section titled “PersistenceError”A message failed to be written to / read from the cluster’s mailbox storage.
Carries the cause. PersistenceError.refail wraps a squashed cause from any
effect.
// _tag: "PersistenceError", cause: DefectRunnerNotRegistered
Section titled “RunnerNotRegistered”A target runner is not registered with the shard manager. Carries its
RunnerAddress. Internal routing/membership failure.
// _tag: "RunnerNotRegistered", address: RunnerAddressRunnerUnavailable
Section titled “RunnerUnavailable”A target runner is unresponsive. Carries its RunnerAddress. Often retryable as
the failure detector and membership view update.
// _tag: "RunnerUnavailable", address: RunnerAddressMailboxFull
Section titled “MailboxFull”A bounded entity mailbox is at capacity. Carries the address. Volatile requests
fail immediately with this; persisted/durable messages are retried or resumed
from storage instead.
// _tag: "MailboxFull", address: EntityAddressAlreadyProcessingMessage
Section titled “AlreadyProcessingMessage”The same request envelope is already in flight — per-envelope duplicate
protection, not a general entity lock. Carries address and envelopeId (a
SnowflakeFromString).
// _tag: "AlreadyProcessingMessage", address: EntityAddress, envelopeId: SnowflakeCatching errors from a client call
Section titled “Catching errors from a client call”When you send to an entity client, handle the errors that surface at that
boundary with Effect.catchTags:
import { Effect } from "effect"
declare const sendToEntity: Effect.Effect< string, | import("effect/unstable/cluster/ClusterError").MailboxFull | import("effect/unstable/cluster/ClusterError").AlreadyProcessingMessage | import("effect/unstable/cluster/ClusterError").PersistenceError>
const program = sendToEntity.pipe( Effect.catchTags({ // back off and retry when the mailbox is saturated MailboxFull: (e) => Effect.logWarning(`mailbox full: ${e.address}`).pipe(Effect.as("dropped")), // the envelope is already being handled — treat as idempotent success AlreadyProcessingMessage: () => Effect.succeed("duplicate"), // storage problem — surface for supervision PersistenceError: (e) => Effect.failCause(e.cause as any) }))Metrics
Section titled “Metrics”ClusterMetrics exports standard gauges describing the shape and health of a
running cluster. They update automatically as the runtime registers entities,
acquires shards, and tracks runner membership — you do not record them yourself.
See Metrics for reading and exporting gauges.
| Export | Metric name | Measures |
|---|---|---|
entities | effect_cluster_entities | Active entity instances on the current runner (tagged by entity type) |
singletons | effect_cluster_singletons | Singleton processes running on the current runner |
runners | effect_cluster_runners | Registered runners known to the cluster |
runnersHealthy | effect_cluster_runners_healthy | Registered runners currently considered healthy |
shards | effect_cluster_shards | Shards currently acquired by the current runner |
All are bigint gauges. entities, singletons, and shards are runner-local
(aggregate per-runner in dashboards); runners and runnersHealthy reflect the
cluster-wide view, which can lag briefly during membership changes.
Reading a gauge value
Section titled “Reading a gauge value”Gauges are ordinary Metric values, so you can read the current snapshot with
Metric.value:
import { Effect, Metric } from "effect"import * as ClusterMetrics from "effect/unstable/cluster/ClusterMetrics"
const program = Effect.gen(function* () { const state = yield* Metric.value(ClusterMetrics.shards) return state // => MetricState.Gauge { value: 7n }})Cluster workflow engine
Section titled “Cluster workflow engine”ClusterWorkflowEngine implements the workflow engine
(WorkflowEngine.WorkflowEngine) on top of cluster entities and persisted
messages, so durable workflows run distributed across the cluster: each
execution becomes a persisted entity, and executions, activities, deferred
completions, resumes, interrupts, and durable-clock wakeups are all coordinated
through persisted cluster entity messages. Workflow names and execution ids
determine the entity address used for persistence, so they must stay stable
across deploys.
You do not author workflows here — define them with the workflow module
(Workflow, Activity, DurableClock, DurableDeferred) and provide this
layer to make them durable and distributed.
ClusterWorkflowEngine.layer
Section titled “ClusterWorkflowEngine.layer”Provides WorkflowEngine.WorkflowEngine backed by the cluster. It requires
Sharding and MessageStorage, and also registers the internal durable-clock
entity used for workflow wakeups.
import { Layer } from "effect"import * as ClusterWorkflowEngine from "effect/unstable/cluster/ClusterWorkflowEngine"
declare const ShardingLayer: Layer.Layer< import("effect/unstable/cluster/Sharding").Sharding>declare const MessageStorageLayer: Layer.Layer< import("effect/unstable/cluster/MessageStorage").MessageStorage>
// Provide a distributed, durable WorkflowEngine to the rest of the app:const WorkflowEngineLayer = ClusterWorkflowEngine.layer.pipe( Layer.provide([ShardingLayer, MessageStorageLayer]))ClusterWorkflowEngine.make
Section titled “ClusterWorkflowEngine.make”The underlying constructor effect (Effect.Effect<WorkflowEngine, never, Sharding | MessageStorage>)
that layer wraps. Prefer layer in applications; reach for make only when
composing the engine into a custom layer.
import { Effect, Layer } from "effect"import * as ClusterWorkflowEngine from "effect/unstable/cluster/ClusterWorkflowEngine"import * as WorkflowEngine from "effect/unstable/workflow/WorkflowEngine"
const customLayer = Layer.effect(WorkflowEngine.WorkflowEngine)( ClusterWorkflowEngine.make)