Skip to content

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.

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.

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"

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)

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

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.

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)

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) // => 3
Snowflake.sequence(id) // => 5
Snowflake.toParts(id)
// => { timestamp: 1735689601000, machineId: 3, sequence: 5 }

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 bigint
Schema.decodeSync(Snowflake.SnowflakeFromBigInt)(123n)
// => 123n (branded)
// string -> branded bigint, and back to string
const 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))

The custom snowflake epoch in Unix milliseconds.

import * as Snowflake from "effect/unstable/cluster/Snowflake"
Snowflake.constEpochMillis
// => 1735689600000 (Date.UTC(2025, 0, 1))

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 callsMailboxFull, 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.

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: EntityAddress

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

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: Defect

A target runner is not registered with the shard manager. Carries its RunnerAddress. Internal routing/membership failure.

// _tag: "RunnerNotRegistered", address: RunnerAddress

A target runner is unresponsive. Carries its RunnerAddress. Often retryable as the failure detector and membership view update.

// _tag: "RunnerUnavailable", address: RunnerAddress

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: EntityAddress

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: Snowflake

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

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.

ExportMetric nameMeasures
entitieseffect_cluster_entitiesActive entity instances on the current runner (tagged by entity type)
singletonseffect_cluster_singletonsSingleton processes running on the current runner
runnerseffect_cluster_runnersRegistered runners known to the cluster
runnersHealthyeffect_cluster_runners_healthyRegistered runners currently considered healthy
shardseffect_cluster_shardsShards 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.

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

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.

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])
)

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
)