Configuration
Ce contenu n’est pas encore disponible dans votre langue.
actor-ts uses HOCON for configuration — a superset of JSON
common in JVM config files, with substitutions, durations and
sizes. (include directives are refused
— compose the sources in code.) Three layers, resolved highest-first:
- Constructor arguments to
ActorSystem.create(name, settings)— explicit code overrides. - User config —
application.confat the project root, or an explicit path viaconfigFile, or an inlineconfigobject in the settings. - Reference defaults — bundled in the framework as
REFERENCE_CONF(seesrc/config/reference.ts).
Anything not specified in layers 1 or 2 falls through to the reference default.
This page explains what each key does. For the exhaustive list — every setting the framework ships, verbatim from the bundled file — see The full reference.conf.
A complete application.conf
Section titled “A complete application.conf”actor-ts { system { name = "my-app" } logger { level = "info" }
dispatcher { default = "immediate" # immediate | microtask | throughput throughput = 16 }
cluster { gossip-interval = 1s seed-retry-interval = 3s weakly-up-after = 0s # 0 = opt-in only max-members = 1000 max-tombstones = 10000 tombstone { time-to-live = 24h prune-interval = 5m min-retention = 0s # 0 = derive from down-after } failure-detector { heartbeat-interval = 500ms unreachable-after = 2s down-after = 5s } pub-sub { gossip-interval = 1s max-subscribers-per-topic = 10000 max-topics = 10000 max-remote-nodes-per-topic = 1000 send-to-dead-letters-when-no-subscribers = on } receptionist { gossip-interval = 1s max-subscribers-per-key = 1000 max-subscribers-total = 10000 } }
distributed-data { gossip-interval = 1s max-pending-quorum-requests = 1000 # 0 = no cap max-quorum-timeout = 30s # 0 = no ceiling }
remote { tcp { host = "0.0.0.0" port = ${?ACTOR_TS_PORT} # env-var substitution; falls back to default } max-frame-bytes = 16M }
persistence { journal { plugin = "actor-ts.persistence.journal.in-memory" } snapshot-store { plugin = "actor-ts.persistence.snapshot-store.in-memory" } }
sharding { number-of-shards = 64 rebalance-interval = 2s hand-off-timeout = 10s remember-entities = false passivation-idle = 5m # 0 disables auto-passivation # shard-passivation-idle # unset: follows passivation-idle max-entities = 0 # 0 = no per-node cap }}Place this at the project root. The framework auto-loads it on
ActorSystem.create.
The keys, grouped by module
Section titled “The keys, grouped by module”actor-ts.system
Section titled “actor-ts.system”| Key | Default | Purpose |
|---|---|---|
system.name | "default" | System name — used in actor paths and cluster identification. Applies when ActorSystem.create() is called without a name; an explicit create('billing') wins. |
actor-ts.logger
Section titled “actor-ts.logger”| Key | Default | Purpose |
|---|---|---|
logger.level | "info" | One of debug / info / warn / error / off. |
actor-ts.dispatcher
Section titled “actor-ts.dispatcher”| Key | Default | Purpose |
|---|---|---|
dispatcher.default | "immediate" | immediate (default) / microtask / throughput. |
dispatcher.throughput | 16 | Messages a ThroughputDispatcher processes before yielding. |
actor-ts.cluster
Section titled “actor-ts.cluster”| Key | Default | Purpose |
|---|---|---|
cluster.gossip-interval | 1s | How often gossip is exchanged with a random peer. |
cluster.seed-retry-interval | 3s | How often to retry seed connections during join. |
cluster.weakly-up-after | 0s | Auto-promote a joining member to weakly-up after this long. 0 disables it — see Weakly-up. |
cluster.max-members | 1000 | Cap on live member entries gossip may introduce. 0 disables the cap. |
cluster.max-tombstones | 10000 | Cap on removed tombstones gossip may introduce. 0 disables the cap. |
cluster.tombstone.time-to-live | 24h | How long a removed tombstone is kept before it is pruned. |
cluster.tombstone.prune-interval | 5m | How often the prune pass runs. |
cluster.tombstone.min-retention | 0s | Floor on tombstone age before pruning is allowed. 0 derives it from failure-detector.down-after (6×). |
cluster.failure-detector.heartbeat-interval | 500ms | How often heartbeats are sent. |
cluster.failure-detector.unreachable-after | 2s | Suspicion threshold for unreachable status. |
cluster.failure-detector.down-after | 5s | Suspicion threshold for forced down. Measured from the last heartbeat, so it must exceed unreachable-after. |
These are layered under whatever Cluster.join(system, options) passes, so
an explicit withGossipIntervalMs(…) still wins. The failure detector
merges per threshold: setting only down-after in code keeps the other
two from the file.
The HOCON tree and the ClusterOptions fields are deliberately not
isomorphic: the three tombstone knobs are grouped under tombstone because
that is how you tune them, while the fields stay flat
(withTombstoneTtlMs(…), …). remote.tcp.host → host already makes the
same translation.
min-retention = 0s means derive the floor from the failure detector,
not no floor — the same thing leaving the field unset means, so a file
that spells the default out behaves like one that omits it.
The two max-* caps bound the local member map. remote.max-frame-bytes
bounds one gossip frame; these bound what a sequence of well-formed frames
can accumulate, which is a different quantity — see
Cluster security.
seeds, roles, the transport and the downing provider have no HOCON
form — the first two are per-deployment identity that belongs at the join
site, the last two are objects a config file cannot express.
actor-ts.cluster.pub-sub
Section titled “actor-ts.cluster.pub-sub”| Key | Default | Purpose |
|---|---|---|
cluster.pub-sub.gossip-interval | 1s | How often a mediator pushes its topic set to a random peer. |
cluster.pub-sub.max-subscribers-per-topic | 10000 | Cap on local subscribers for one topic. A Subscribe past it is answered with SubscribeRejected. |
cluster.pub-sub.max-topics | 10000 | Cap on distinct topics one mediator tracks — enforced against gossiped topic claims as well as local subscribes. |
cluster.pub-sub.max-remote-nodes-per-topic | 1000 | Cap on peers that may claim subscribers for one topic. |
cluster.pub-sub.send-to-dead-letters-when-no-subscribers | on | Route a publish that reached nobody to system.deadLetters instead of discarding it. |
The three caps bound what one mediator can be made to hold, and publish
fan-out walks all of them — so they are a latency bound as much as a
memory one. The gossip half is the one worth knowing about: a peer
announcing topics it claims subscribers for used to allocate an entry per
name on every receiver, with no local Subscribe involved. See
DistributedPubSub.
actor-ts.cluster.receptionist
Section titled “actor-ts.cluster.receptionist”| Key | Default | Purpose |
|---|---|---|
cluster.receptionist.gossip-interval | 1s | How often the receptionist gossips its local registrations. |
cluster.receptionist.max-subscribers-per-key | 1000 | Cap on subscribers for one service key. A Subscribe past it is answered with ReceptionistSubscribeRejected. |
cluster.receptionist.max-subscribers-total | 10000 | Cap on subscribers across every key on this node. |
Both blocks layer under whatever the extension’s start(...) was passed,
so an explicit withMaxTopics(…) still wins. Stopped subscribers are
released by death watch and do not count against either cap.
actor-ts.distributed-data
Section titled “actor-ts.distributed-data”| Key | Default | Purpose |
|---|---|---|
distributed-data.gossip-interval | 1s | How often a replica pushes its full key set to a random peer. |
distributed-data.max-pending-quorum-requests | 1000 | Cap on unsettled quorum requests — updateAsync and getAsync share one budget. A request past it is rejected outright. 0 disables the cap. |
distributed-data.max-quorum-timeout | 30s | Ceiling on a caller’s per-call timeoutMs; a larger value is clamped down to this one. 0 disables the ceiling. |
The block is top-level rather than under cluster.* (where pub-sub and
receptionist live) because the module is: DistributedData ships from
src/crdt/, and the cluster is a positional argument to start(cluster)
rather than one of its tunables. durableStore has no key here — it is a
DurableStateStore instance, which a config file cannot express.
Both caps bound the unsettled set itself. Every pending quorum request holds
a promise, a timer and a target set alive until its deadline passes, so an
uncapped replicator under load accumulates all three with nothing to stop it.
Refusing past max-pending-quorum-requests turns what would arrive later as a
timeout storm into immediate rejections that name the knob. See
Quorum reads and writes.
max-quorum-timeout bounds the other axis. A pending request holds one of
those slots for its whole deadline, so a single caller passing an hour-long
timeoutMs parks the budget for an hour and locks out later callers who
never came near the cap themselves.
actor-ts.remote
Section titled “actor-ts.remote”| Key | Default | Purpose |
|---|---|---|
remote.tcp.host | "0.0.0.0" | Bind address. Used when ClusterOptions leaves host unset. |
remote.tcp.port | 2552 | Bind port. Used when ClusterOptions leaves port unset. |
remote.max-frame-bytes | 16M | Per-frame wire cap. A frame whose length-prefix exceeds it is rejected before any payload is buffered. |
remote.tls.enabled | false | Not implemented — see below. |
Because the bind address is readable from config, a deployment can move it out of code entirely:
actor-ts.remote.tcp { host = "0.0.0.0" port = ${?ACTOR_TS_PORT} # env-var substitution}remote.max-frame-bytes applies to the transport the cluster builds for
itself. If you construct a TcpTransport yourself and pass it via
withTransport(…), set its cap there — the framework will not re-cap a
transport it did not create.
actor-ts.http
Section titled “actor-ts.http”| Key | Default | Purpose |
|---|---|---|
http.backend | "fastify" | One of fastify / express / hono. |
http.shutdown-grace-period | 0ms | How long unbind() lets in-flight requests drain before connections are forced. 0 forces immediately. |
http.websocket | (see below) | Server-side WebSocket defaults for websocket() routes. |
http.backend only decides what newServerAt(...).bind() uses when the
builder was given no backend of its own — useBackend(new HonoBackend())
always wins, and is still the only way to pass a backend you configured or
wrote yourself. An unrecognised name fails the bind() with a
ConfigError rather than silently falling back to Fastify. Express and
Hono are optional peer dependencies, imported only if you name them.
The grace period defaults to 0 — force as soon as the server closes —
because that is what unbind() has always done in practice. Raise it if
you want in-flight requests to finish.
It is usually an upper bound rather than a delay: unbind() returns as soon
as the server has actually closed. Where a backend’s close() cannot
settle — Express holding a live WebSocket, for one — the window becomes a
deadline that is always reached, so a large value there is time you always
pay. A caller that passes its own value — binding.unbind(1_000) —
overrides the config.
Server-side WebSocket routes (websocket(path, actorRef)) read their
defaults from actor-ts.http.websocket:
| Key | Default | Purpose |
|---|---|---|
http.websocket.maxFrameBytes | 1M | Largest inbound frame accepted (bytes). |
http.websocket.onOversizeFrame | "close" | What to do when a frame exceeds the limit — close / drop. |
http.websocket.onInvalidMessage | "close" | What to do when a message fails to decode — close / drop / hook. |
http.websocket.maxBufferedBytes | 4M | Outbound backpressure threshold (bytes). |
http.websocket.onBackpressure | "drop" | What to do when the buffer is exceeded — drop / close. |
http.websocket.maxConnections | (unlimited) | Max concurrent connections per route; a new upgrade beyond it is closed with 1013. |
actor-ts.persistence
Section titled “actor-ts.persistence”| Key | Default | Purpose |
|---|---|---|
persistence.journal.plugin | "...in-memory" | Fully-qualified key to the journal’s config. |
persistence.snapshot-store.plugin | "...in-memory" | Fully-qualified key to the snapshot store’s config. |
The plugin keys point to another config section that holds
that plugin’s settings. E.g. actor-ts.persistence.journal.sqlite
contains the SQLite journal’s path, pragmas, etc. See each
journal page in Persistence for
the per-plugin keys.
actor-ts.cache
Section titled “actor-ts.cache”| Key | Default | Purpose |
|---|---|---|
cache.in-memory.maxEntries | 10000 | Max entries before LRU eviction (in-memory cache). |
cache.in-memory.cleanupMs | 60000 | Interval (ms) of the background sweep for expired entries. |
Redis and Memcached caches read their connection settings from
actor-ts.cache.redis / actor-ts.cache.memcached.
actor-ts.sharding
Section titled “actor-ts.sharding”| Key | Default | Purpose |
|---|---|---|
sharding.number-of-shards | 64 | How many shards the entity space is divided into. |
sharding.rebalance-interval | 2s | Gap between coordinator-driven rebalance passes. |
sharding.hand-off-timeout | 10s | How long to wait for HandOffComplete before force-reallocating. |
sharding.remember-entities | false | Persist the set of active entity IDs. |
sharding.passivation-idle | 5m | Auto-passivate an entity after this idle window. 0 = disabled. An entity that keeps state in memory and does not rebuild it in preStart loses that state — see passivation. |
sharding.shard-passivation-idle | (unset) | Auto-passivate a shard after it has stood empty this long. Unset it follows passivation-idle; 0 keeps empty shards resident. Deliberately absent from reference.conf, because a shipped value is what “unset” would have to be distinguishable from. |
sharding.max-entities | 0 | Per-node entity cap; the least-recently-used entity is passivated on overflow. 0 = no cap. |
These apply to every sharded type started on the node. They are read
once per sharding.start(...) and layered under whatever that call passes
explicitly, so the usual order holds per field:
// actor-ts.sharding.passivation-idle = 2 minutes
const shardingOptions = StartShardingOptions.create<CartCommand>() .withTypeName('cart') .withEntityActor(CartEntity) .withExtractEntityId((command) => command.entityId) .withNumShards(256);
cluster.sharding.start(shardingOptions);// numShards 256 (explicit), passivationIdleMs 120_000 (config file)A per-type setting therefore belongs in the builder; the config file is for the node-wide baseline you want to move between staging and production without a rebuild. The polymorphic options — the entity actor, the extractors, the allocation strategy, a lease, the stores — have no HOCON form and stay in code.
actor-ts.coordinated-shutdown
Section titled “actor-ts.coordinated-shutdown”| Key | Default | Purpose |
|---|---|---|
coordinated-shutdown.default-phase-timeout | 5s | Default per-phase timeout. A phase that overruns it is abandoned so the next one still runs. |
coordinated-shutdown.terminate-actor-system | true | Whether the built-in terminator task calls system.terminate() in the final phase. |
coordinated-shutdown.exit-process | false | Whether to call process.exit(0) once the pipeline completes. |
Setting terminate-actor-system = false removes only the built-in
terminator — user tasks registered in ActorSystemTerminate still run. Use
it when a host process owns the system’s lifetime and a signal handler must
not kill it.
exit-process is for the opposite case: a lingering handle (an open socket,
a pooled connection a driver never released) keeps the process alive after
the pipeline is done, and from the outside that is indistinguishable from a
hang. Off by default, because forcing an exit hides exactly the leaks you
would rather find.
actor-ts.worker-cluster (multi-runtime workers)
Section titled “actor-ts.worker-cluster (multi-runtime workers)”| Key | Default | Purpose |
|---|---|---|
worker-cluster.workers | "auto" | Number of workers — "auto" uses navigator.hardwareConcurrency. |
worker-cluster.restart-policy | "on-failure" | always / on-failure / never. |
These fill in WorkerCluster.spawn(options) for fields it leaves unset.
spawn is a static with no ActorSystem in scope, so it loads the config
chain itself — the same one ActorSystem.create uses, honouring
ACTOR_TS_CONFIG and ./application.conf. An unknown restart-policy is
rejected with an OptionsError instead of silently meaning “never”.
Broker plugins — actor-ts.io.broker.*
Section titled “Broker plugins — actor-ts.io.broker.*”Each broker actor reads from its own subtree. See the per-protocol pages for the keys:
| Subtree | Page |
|---|---|
io.broker.kafka | Kafka |
io.broker.mqtt | MQTT |
io.broker.amqp | AMQP |
io.broker.nats | NATS |
io.broker.jetstream | NATS JetStream |
io.broker.jetstream-key-value | JetStream KV + Object Store |
io.broker.jetstream-object-store | JetStream KV + Object Store |
io.broker.redis-streams | Redis Streams |
io.broker.grpc.{client,server} | gRPC |
io.broker.websocket | WebSocket client |
io.broker.sse | SSE |
io.broker.tcp | TCP |
io.broker.udp | UDP |
HOCON-specific syntax
Section titled “HOCON-specific syntax”Durations
Section titled “Durations”gossip-interval = 1s # 1000 msunreachable-after = 2.5s # 2500 msdown-after = 5000ms # explicit msgc-cadence = 10m # minutesttl = 24h # hoursRecognized units: ns, us, ms, s, m, h, d.
max-frame-bytes = 16M # 16 777 216 bytesbuffer = 64Kheap = 2GRecognized units: B, K, M, G, T — binary (1024) by
default.
Environment substitution
Section titled “Environment substitution”port = ${?ACTOR_TS_PORT} # use env if set, else fall throughlog-level = ${?LOG_LEVEL} # sameapi-key = ${API_KEY} # required — error if env is unsetfallback-port = ${?ENV_PORT}fallback-port = ${fallback-port:-2552} # default-if-empty syntaxUse ${?ENV} for optional, ${ENV} for required.
Includes are not supported
Section titled “Includes are not supported”include "shared-cluster.conf" # error: `include` is not supportedA config source cannot name another file or URL to pull in. That is a decision, not a gap: resolving an include would let whoever writes the config choose which paths the process reads, relative to a root the parser has no way to know. Merging in code leaves that choice with the caller.
import { Config } from 'actor-ts';
const shared = Config.parseFile('shared-cluster.conf');const application = Config.parseFile('application.conf');const config = shared.merge(application);merge layers its argument on top, so the local file wins — the same
order the include above was reaching for. Pasting the shared content
into one file works too.
The parser says all of this when it meets an include, quoting the line
and the target, so a config ported from Akka or Pekko fails with an
answer rather than a puzzle.
Reserved keys
Section titled “Reserved keys”Three keys are refused anywhere in a config source — as a key, as any segment of a path expression, and inside a substitution:
__proto__.anything = 1 # error: refusedconstructor.x = 1 # error: refusedprototype = 1 # error: refusedvalue = ${__proto__} # error: refusedA key path is expanded onto a plain object, and assigning __proto__ writes
through to the object prototype rather than creating a normal key — so allowing
it would let a config file change the behaviour of every object in the process.
The parser rejects these with the usual line and column instead.
The check is exact-match, so keys that merely resemble them are fine:
_proto_, constructorName and prototypes all work normally.
Reading config in code
Section titled “Reading config in code”const actorSystemOptions = ActorSystemOptions.create().withConfig({ 'actor-ts.cluster.gossip-interval': '500ms', });const system = ActorSystem.create( 'my-app', actorSystemOptions,);
// Inside an actor / extension:const config = system.config;const interval = config.getDuration('actor-ts.cluster.gossip-interval');// → 500 (in ms)The Config interface has typed getters: getString, getNumber,
getBoolean, getDuration, getBytes, getStringList, etc.
Missing keys throw unless you check first with hasPath.
Loading order
Section titled “Loading order”1. REFERENCE_CONF ← bundled defaults2. application.conf (CWD) ← project root, auto-loaded3. file path from `configFile` setting ← explicit override4. ENV var ACTOR_TS_CONFIG ← path or inline HOCON5. constructor `config: { ... }` ← code-level override6. constructor field overrides ← explicit fields beat config (logLevel, dispatcher, scheduler, …)Each layer overlays on top of the previous. Layer 6 (constructor fields) always wins for the fields it covers; everything else flows through the HOCON merge.
Where the built-in default lives
Section titled “Where the built-in default lives”Layer 1 is reference.conf, but not every setting has a key there — and
even those that do need a value in code for the case where nothing is
configured. That value has a fixed home, so you can always find what a
setting falls back to:
- Options defaults sit next to the field they back, in the matching
XOptions.ts.DEFAULT_MAX_ENTRIESis inInMemoryCacheOptions.ts,DEFAULT_NUM_SHARDSinShardingOptions.ts. - Everything else — caps, bounds and timeouts with no option behind
them — is in a
Constants.ts:src/<subsystem>/Constants.tsfor one subsystem,src/util/Constants.tsfor values several share.
So a reference.conf leaf and its code-level fallback are always exactly
two files, and the second one is named after the first.
Validation
Section titled “Validation”Option values are validated once, at consume time, on the merged
settings — after the three layers above are resolved. Because it runs
on the merged result, the same rules apply no matter how a value
arrived: the fluent builder, a plain settings object, or a HOCON key.
Cross-field rules (e.g. “downThreshold must exceed
unreachableThreshold”) see the final values.
An invalid value throws an OptionsError (exported from the package
root), naming the offending field and what it expected:
MqttOptions: protocolVersion must be one of 4, 5 (got 6)- For broker actors (MQTT, Kafka, WebSocket client, …) the error is
raised when the actor starts (
preStart), after the required-field check — so a missing required setting still surfaces asBrokerOptionsError, and only present-but-invalid values becomeOptionsError. - For non-broker consumers (caches, leases, sharding, …) the error is raised from the constructor.
OptionsError is distinct from ConfigError (malformed HOCON — wrong
type, missing path) and BrokerOptionsError (a required broker setting
absent from every layer). Unset optional fields never trigger it; they
fall through to their defaults.
import { CircuitBreaker, CircuitBreakerOptions, OptionsError } from 'actor-ts';
// Non-broker consumers validate in their constructor, so the error is// thrown synchronously and a plain try/catch works:try { const options = CircuitBreakerOptions.create() .withMaxFailures(0); // invalid — must be a positive integer new CircuitBreaker(options); // validates in the constructor} catch (e) { if (e instanceof OptionsError) { console.error(`bad option ${e.field}: ${e.message}`); }}// For a broker actor the OptionsError is thrown later in preStart and// re-wrapped as ActorInitializationError, so it surfaces via supervision// (not the spawning call's try/catch).Where to next
Section titled “Where to next”- Actor system — how settings reach the framework.
- Cluster overview — the keys most relevant to multi-node setups.
- Persistence overview — journal + snapshot-store plugin selection.
- Version policy — what’s stable vs experimental.
