콘텐츠로 이동
한국어

Configuration

이 콘텐츠는 아직 번역되지 않았습니다.

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:

  1. Constructor arguments to ActorSystem.create(name, settings) — explicit code overrides.
  2. User configapplication.conf at the project root, or an explicit path via configFile, or an inline config object in the settings.
  3. Reference defaults — bundled in the framework as REFERENCE_CONF (see src/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.

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.

KeyDefaultPurpose
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.
KeyDefaultPurpose
logger.level"info"One of debug / info / warn / error / off.
KeyDefaultPurpose
dispatcher.default"immediate"immediate (default) / microtask / throughput.
dispatcher.throughput16Messages a ThroughputDispatcher processes before yielding.
KeyDefaultPurpose
cluster.gossip-interval1sHow often gossip is exchanged with a random peer.
cluster.seed-retry-interval3sHow often to retry seed connections during join.
cluster.weakly-up-after0sAuto-promote a joining member to weakly-up after this long. 0 disables it — see Weakly-up.
cluster.max-members1000Cap on live member entries gossip may introduce. 0 disables the cap.
cluster.max-tombstones10000Cap on removed tombstones gossip may introduce. 0 disables the cap.
cluster.tombstone.time-to-live24hHow long a removed tombstone is kept before it is pruned.
cluster.tombstone.prune-interval5mHow often the prune pass runs.
cluster.tombstone.min-retention0sFloor on tombstone age before pruning is allowed. 0 derives it from failure-detector.down-after (6×).
cluster.failure-detector.heartbeat-interval500msHow often heartbeats are sent.
cluster.failure-detector.unreachable-after2sSuspicion threshold for unreachable status.
cluster.failure-detector.down-after5sSuspicion 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.hosthost 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.

KeyDefaultPurpose
cluster.pub-sub.gossip-interval1sHow often a mediator pushes its topic set to a random peer.
cluster.pub-sub.max-subscribers-per-topic10000Cap on local subscribers for one topic. A Subscribe past it is answered with SubscribeRejected.
cluster.pub-sub.max-topics10000Cap on distinct topics one mediator tracks — enforced against gossiped topic claims as well as local subscribes.
cluster.pub-sub.max-remote-nodes-per-topic1000Cap on peers that may claim subscribers for one topic.
cluster.pub-sub.send-to-dead-letters-when-no-subscribersonRoute 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.

KeyDefaultPurpose
cluster.receptionist.gossip-interval1sHow often the receptionist gossips its local registrations.
cluster.receptionist.max-subscribers-per-key1000Cap on subscribers for one service key. A Subscribe past it is answered with ReceptionistSubscribeRejected.
cluster.receptionist.max-subscribers-total10000Cap 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.

KeyDefaultPurpose
distributed-data.gossip-interval1sHow often a replica pushes its full key set to a random peer.
distributed-data.max-pending-quorum-requests1000Cap 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-timeout30sCeiling 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.

KeyDefaultPurpose
remote.tcp.host"0.0.0.0"Bind address. Used when ClusterOptions leaves host unset.
remote.tcp.port2552Bind port. Used when ClusterOptions leaves port unset.
remote.max-frame-bytes16MPer-frame wire cap. A frame whose length-prefix exceeds it is rejected before any payload is buffered.
remote.tls.enabledfalseNot 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.

KeyDefaultPurpose
http.backend"fastify"One of fastify / express / hono.
http.shutdown-grace-period0msHow 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:

KeyDefaultPurpose
http.websocket.maxFrameBytes1MLargest 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.maxBufferedBytes4MOutbound 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.
KeyDefaultPurpose
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.

KeyDefaultPurpose
cache.in-memory.maxEntries10000Max entries before LRU eviction (in-memory cache).
cache.in-memory.cleanupMs60000Interval (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.

KeyDefaultPurpose
sharding.number-of-shards64How many shards the entity space is divided into.
sharding.rebalance-interval2sGap between coordinator-driven rebalance passes.
sharding.hand-off-timeout10sHow long to wait for HandOffComplete before force-reallocating.
sharding.remember-entitiesfalsePersist the set of active entity IDs.
sharding.passivation-idle5mAuto-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-entities0Per-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.

KeyDefaultPurpose
coordinated-shutdown.default-phase-timeout5sDefault per-phase timeout. A phase that overruns it is abandoned so the next one still runs.
coordinated-shutdown.terminate-actor-systemtrueWhether the built-in terminator task calls system.terminate() in the final phase.
coordinated-shutdown.exit-processfalseWhether 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)”
KeyDefaultPurpose
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”.

Each broker actor reads from its own subtree. See the per-protocol pages for the keys:

SubtreePage
io.broker.kafkaKafka
io.broker.mqttMQTT
io.broker.amqpAMQP
io.broker.natsNATS
io.broker.jetstreamNATS JetStream
io.broker.jetstream-key-valueJetStream KV + Object Store
io.broker.jetstream-object-storeJetStream KV + Object Store
io.broker.redis-streamsRedis Streams
io.broker.grpc.{client,server}gRPC
io.broker.websocketWebSocket client
io.broker.sseSSE
io.broker.tcpTCP
io.broker.udpUDP
gossip-interval = 1s # 1000 ms
unreachable-after = 2.5s # 2500 ms
down-after = 5000ms # explicit ms
gc-cadence = 10m # minutes
ttl = 24h # hours

Recognized units: ns, us, ms, s, m, h, d.

max-frame-bytes = 16M # 16 777 216 bytes
buffer = 64K
heap = 2G

Recognized units: B, K, M, G, T — binary (1024) by default.

port = ${?ACTOR_TS_PORT} # use env if set, else fall through
log-level = ${?LOG_LEVEL} # same
api-key = ${API_KEY} # required — error if env is unset
fallback-port = ${?ENV_PORT}
fallback-port = ${fallback-port:-2552} # default-if-empty syntax

Use ${?ENV} for optional, ${ENV} for required.

include "shared-cluster.conf" # error: `include` is not supported

A 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.

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: refused
constructor.x = 1 # error: refused
prototype = 1 # error: refused
value = ${__proto__} # error: refused

A 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.

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.

1. REFERENCE_CONF ← bundled defaults
2. application.conf (CWD) ← project root, auto-loaded
3. file path from `configFile` setting ← explicit override
4. ENV var ACTOR_TS_CONFIG ← path or inline HOCON
5. constructor `config: { ... }` ← code-level override
6. 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.

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_ENTRIES is in InMemoryCacheOptions.ts, DEFAULT_NUM_SHARDS in ShardingOptions.ts.
  • Everything else — caps, bounds and timeouts with no option behind them — is in a Constants.ts: src/<subsystem>/Constants.ts for one subsystem, src/util/Constants.ts for 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.

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 as BrokerOptionsError, and only present-but-invalid values become OptionsError.
  • 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).