The full reference.conf
Ce contenu n’est pas encore disponible dans votre langue.
This is the framework’s built-in configuration, reproduced exactly as it
ships in src/config/reference.ts. It is the complete set of
first-party settings: anything not here has no HOCON form, and anything
here is read by something (a test fails the build otherwise — see
No dead keys below).
Every value shown is the default already in effect, so you never need to
copy the whole thing. Put only what you want to change in your
application.conf:
# application.conf — everything else keeps the defaults belowactor-ts { system.name = "billing" sharding.passivation-idle = 2 minutes}For what each key does, see Configuration — this page is the exhaustive list, that one is the explanation.
The file
Section titled “The file”actor-ts { system { name = "default"
# How long terminate() lets the actors under /user finish the work they # already have before the stop cascade starts. The wait ends as soon as # the tree goes quiet, so an idle system pays a tick rather than the # budget; what is still queued when the budget runs out is dead-lettered # exactly as it was before. Keep it under # coordinated-shutdown.default-phase-timeout — the last phase awaits # terminate(), so a drain as long as the phase leaves no room to stop # anything. 0 disables draining and restores the pre-#663 behaviour. # # A throttled or suspended mailbox is never waited on: neither drains at # a rate a shutdown can wait for. shutdown-drain-timeout = 2s }
logger { # System log level. Gates BEFORE the per-sink min-levels below, so a # sink asking for "debug" while this says "info" receives nothing — # lower this first, then narrow per sink. level = "info" # debug | info | warn | error | off
# Grace period terminate() gives the active logger to flush and close # its sinks before whenTerminated() resolves. Bounds the whole logger, # so it applies to a custom one too — any logger with a close(). close-timeout = 3s
# Multi-sink pipeline. Every sink ships disabled; enabling at least one # replaces the default single ConsoleLogger with a MultiSinkLogger over # the enabled set. An explicit logger (or logSinks) passed to # ActorSystem.create replaces this whole block rather than merging with # it -- a sink belongs to one construction world or the other. # # The Sentry sink has no block here: it needs your own @sentry/node # import, which a config file cannot hold, so it is wired in code. # See docs -> Observe -> Logging -> Platform integrations. sinks { console { enabled = false min-level = "info" # debug | info | warn | error | off format = "text" # text = human-readable, json = one NDJSON object per record stream = "auto" # auto | stdout | stderr; auto = console.* for text, stdout for json }
file { enabled = false min-level = "info" format = "text" directory = "logs" # created if missing prefix = "log" # file name is <prefix>-<yyyy-MM-dd>-<HH-mm-ss>.<extension> extension = "txt" # Rolling over always opens a NEW stamped file — the active file is # never renamed, which Windows forbids while it is open anyway. max-file-bytes = 64M # roll when the file would pass this; 0 = never rotate-interval = "daily" # off | hourly | daily — roll on the clock boundary # Retention only ever deletes files matching this sink's own prefix, # extension and timestamp shape — never anything else in the directory. max-files = 14 # keep this many rotated files; 0 = keep all max-age = 14d # delete rotated files older than this; 0 = keep all compress-rotated = false # gzip each rotated file to <name>.gz
delivery { max-batch-size = 500 flush-interval = 1s queue-capacity = 10000 overflow = "drop-new" # drop-new | drop-head } }
# Graylog. Native rather than via OTLP because Graylog's # OpenTelemetry input speaks gRPC only, which the otlp sink does not. gelf { enabled = false min-level = "info" protocol = "udp" # udp | tcp | http host = "127.0.0.1" # udp/tcp port = 12201 # udp/tcp url = "" # http only, e.g. "http://graylog:12201/gelf" # The GELF "host" field. Empty = the OS hostname, else the system name. host-name = "" compression = "gzip" # udp only: none | gzip (server auto-detects) max-chunk-bytes = 1420 # udp only: datagram size before chunking request-timeout = 10s # http only # TLS for tcp is code-only: those fields carry the key material # itself, not a path to it. delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# OpenTelemetry logs over HTTP with a JSON body. One endpoint format # reaches Loki 3+, Parseable, SigNoz, Datadog, Axiom, Honeycomb, New # Relic and every OTel Collector — start here before a native sink. otlp { enabled = false min-level = "info" url = "http://localhost:4318/v1/logs" # service.name on the OTLP resource; defaults to the system name. service-name = "" scope-name = "actor-ts" gzip = false request-timeout = 10s # Request headers (API keys, tenant ids) are code-only: a config # file is the wrong home for a credential. delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Grafana Loki's native push API. Loki 3+ also accepts OTLP at # /otlp/v1/logs, so the otlp sink reaches it too; this one exists for # direct push and explicit control over the label set. loki { enabled = false min-level = "info" url = "" # base URL, e.g. "http://loki:3100" tenant-id = "" # X-Scope-OrgID, for multi-tenant Loki format = "text" # text | json — how each log line is rendered # Labels are Loki's INDEX. Keep them static and few: every distinct # combination is a separate stream, and a per-record value here # (an actor path, a request id) multiplies streams without bound. # Everything variable rides as structured metadata instead. labels { service = "" # empty = the actor system's name } structured-metadata = true request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Parseable's REST ingestion. Its OTLP endpoint works too — this # sink sends a flatter record and skips OTLP semantics. parseable { enabled = false min-level = "info" url = "" # base URL, e.g. "https://parseable.internal" stream = "" # target dataset; created on first use # Basic auth OR an API key, never both. Prefer a substitution # (${?PARSEABLE_API_KEY}) over writing a secret in here. username = "" password = "" api-key = "" request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Seq, over CLEF — newline-delimited JSON with @-prefixed reserved # keys. Close enough to the framework's own NDJSON that there is # almost nothing to translate. seq { enabled = false min-level = "info" url = "" # base URL, e.g. "http://seq:5341" api-key = "" # X-Seq-ApiKey; prefer ${?SEQ_API_KEY} request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# Splunk's HTTP Event Collector. splunk { enabled = false min-level = "info" url = "" # HEC base URL, e.g. "https://splunk:8088" token = "" # HEC token; prefer ${?SPLUNK_HEC_TOKEN} index = "" # empty = the token's default index source = "actor-ts" sourcetype = "_json" host-name = "" # empty = the actor system's name request-timeout = 10s delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } }
# RFC 5424 syslog — the integration that needs no vendor: rsyslog, # syslog-ng, journald's forwarder, Papertrail and a long tail of # appliances all speak it. syslog { enabled = false min-level = "info" transport = "udp" # udp | tcp | tls host = "127.0.0.1" port = 514 facility = 16 # 0-23; 16 = local0, the range for applications app-name = "" # empty = the actor system's name host-name = "" # empty = the OS hostname # tcp/tls framing. octet-counting (RFC 6587) is the only one that # survives a message containing a newline — a stack trace always does. framing = "octet-counting" # octet-counting | lf # TLS material is code-only: those fields carry the key itself. delivery { max-batch-size = 100 flush-interval = 2s queue-capacity = 10000 } } } }
actor { # User messages ONE actor handles per dispatcher turn before it yields. # Distinct from dispatcher.throughput below, which bounds how many queued # units — each belonging to a different actor — a ThroughputDispatcher # drains per tick. This is the knob that amortises the scheduling round # trip, because a cell may only ever have one unit queued at a time. # 1 restores the pre-#409 message-at-a-time interleaving; raising it trades # fairness for throughput, since nothing else on the event loop runs until # the actor yields. Override per actor with ActorOptions.withThroughput(). throughput = 16 }
dispatcher { # hybrid | immediate | microtask | throughput # # hybrid wakes actors on the microtask queue and spends every 64th unit on # a macrotask so timers and I/O still run; immediate is the previous # default, one setImmediate per turn — fair, and ~2.4 us a hop that a # request/response actor cannot amortise. microtask is the unbounded form # and starves the event loop under a sustained volley; it is kept for # measurement, not for production. default = "hybrid" # Queued units a ThroughputDispatcher drains per tick, ACROSS actors — # see actor.throughput above for the per-actor batch. throughput = 16 }
# Bounded record of the messages the system could not deliver, inspectable # and replayable through system.deadLetterQueue. Undeliverable messages are # published on the event stream either way -- this decides whether anything # KEEPS them, which by default nothing does. dead-letters { # One axis, ordered by how much of the letter is kept: # off = publish only, keep nothing (the behaviour before #433) # metrics = count them and keep no payload, for the alert without the # evidence locker (retaining a message is a data-protection # decision; observing a rate is not) # memory = keep in a bounded ring, lost with the process # persistent = additionally write to the configured journal, so the queue # is still there after a restart store = "off"
# Letters held before the oldest is evicted. The queue is a diagnostic # ring: an unbounded one turns a delivery outage into an out-of-memory. # Ignored by the off and metrics stores, which hold no letters. max-entries = 1000
# Age letters out after this long. 0 disables ageing and leaves # max-entries as the only bound. retention = 1h
# How often one letter may be replayed before it is quarantined. A # replayed message that dead-letters again comes back as the SAME entry # with a higher count, so a poison message cannot be retried forever. max-replays = 3
# Journal stream the persistent store writes to. Empty = derive it from # the system name, so two systems sharing a journal keep separate queues. persistence-id = "" }
cluster { gossip-interval = 1s seed-retry-interval = 3s weakly-up-after = 0s # 0 disables auto weakly-up promotion
# Caps on the local member map. max-frame-bytes bounds ONE gossip frame; # these bound what a sequence of well-formed frames can accumulate, since # gossip is what introduces addresses in the first place. 0 disables # either. max-tombstones is the load-bearing one: a tombstone carries no # liveness, so nothing but the TTL below ever reclaims it. max-members = 1000 max-tombstones = 10000
tombstone { time-to-live = 24h prune-interval = 5m min-retention = 0s # 0 = derive from failure-detector down-after }
failure-detector { heartbeat-interval = 500ms unreachable-after = 2s down-after = 5s # measured from the last heartbeat, so > unreachable-after }
# Stable-observation bootstrap: poll discovery until the contact-point set # has been unchanged for stable-margin, then let the lowest-addressed node # -- and only it -- form a cluster if no peer promoted it within # self-election-grace. Opt-in: bootstrapCluster reads this block only when # its stableObservation option is set. required-contact-points is the one # knob worth changing: 1 keeps single-node development working, but only a # value matching the expected replica count catches discovery that is # stably wrong rather than merely slow. bootstrap { stable-margin = 5s poll-interval = 1s max-wait = 60s required-contact-points = 1 self-election-grace = 10s
# Fewest up members -- self included -- before bootstrapCluster's # awaitReady (and Cluster.awaitReady / isReady) counts the cluster as # ready. 1 keeps single-node development working; a deployment states # its replica count here, for the same reason as # required-contact-points. minimum-members = 1
# await-ready ships no value on purpose -- a key that is always present # could not express "unset", and unset is what selects the computed # default: self-election-grace + 5s behind stable observation, 5s # otherwise (#1086). Set a duration to pin the budget regardless: # # await-ready = 30s }
# Cluster-wide publish/subscribe (DistributedPubSub). The caps bound what # one mediator can be made to hold -- by local subscribers and by a peer's # gossiped topic claims alike. A Subscribe over a cap is answered with # SubscribeRejected, never silently dropped. pub-sub { gossip-interval = 1s max-subscribers-per-topic = 10000 max-topics = 10000 max-remote-nodes-per-topic = 1000 # A publish that reached no subscriber goes to system.deadLetters, so a # mistyped topic is observable instead of silent. off = discard it. send-to-dead-letters-when-no-subscribers = on }
# Cluster-wide service registry (Receptionist). Subscribers are watched, # so a stopped one is dropped; the caps bound the ones that are still alive. # The total counts key/subscriber pairs, not distinct subscribers — one # subscriber on three keys spends three of it. receptionist { gossip-interval = 1s max-subscribers-per-key = 1000 max-subscriptions-total = 10000 } }
# Cluster-wide replicated CRDT store (DistributedData). Top-level rather # than under cluster.* because the module is -- the cluster is a positional # argument to start(), not a tunable. Both caps bound quorum requests # (updateAsync + getAsync); 0 disables either. What they buy is a bound on # the unsettled set itself: every entry holds a promise, a timer and a # target set until its deadline passes, so refusing past the cap turns what # would be a timeout storm into immediate, attributable rejections. # # max-gossip-bytes bounds one outbound gossip frame instead; a larger store # is pushed a slice per tick. It is clamped down to remote.max-frame-bytes, # because a frame past that cap is rejected on its length prefix and costs # the whole peer association -- heartbeats included. 0 removes the budget # (the clamp still applies). distributed-data { gossip-interval = 1s max-pending-quorum-requests = 1000 max-quorum-timeout = 30s max-gossip-bytes = 1M }
remote { # Bind address of this node. Cluster.join reads these when its options # leave host/port unset, so a deployment can move the address into config. # # host is the interface to BIND, and the wildcard below is the right # default for it. It is not an identity: what peers dial is # advertised-host, which falls back to host only when host is not a # wildcard, then to CLUSTER_HOST / POD_IP / HOSTNAME, then to 127.0.0.1. # advertised-host ships no value on purpose -- a key that is always # present could not express "unset", and unset is what makes that # fallback chain reachable. Set it wherever the bound interface and the # dialable address differ, which in Kubernetes is every pod: # # advertised-host = ${?POD_IP} # # Leaving it at the wildcard in a multi-node deployment is the failure it # exists to prevent: every node advertises the identical # system@0.0.0.0:2552, each reads the others' announcements as claims # about itself, and every member map ends up holding one entry. tcp { host = "0.0.0.0" port = 2552 } tls { # Read but NOT honoured: the transport the cluster builds for itself is # always plaintext. Setting this to true only buys a startup WARN that # says so — encrypting the wire is issue #941. enabled = false } max-frame-bytes = 16M # per-frame wire cap; lower it on semi-trusted networks }
http { backend = "fastify" # fastify | express | hono # In-flight drain window for unbind() before connections are forced. # 0 keeps the historical behaviour (force immediately); raise it if you # want in-flight requests to finish on shutdown. shutdown-grace-period = 0ms
# Outbound HttpClient defaults — the system's shared client and any # newClient(...) that leaves a field unset. Leaf names match the # HttpClientOptions fields (camelCase) and are validated on read (a bad # value throws OptionsError). A request may still override each of them # per call; these are the floor the fleet inherits without a code change. client { maxResponseBytes = 8M # buffered response body ceiling defaultTimeoutMs = 30s # deadline for a call that names none redirect = "follow" # follow | error | manual maxRedirects = 5 # hops a followed chain may take }
# Server-side defaults for websocket() routes (per-connection policy). # Leaf names match the WebsocketRouteOptions fields (camelCase); a route # may override any of them, and the resolved values are validated # (OptionsError on a bad value). websocket { maxFrameBytes = 1M # inbound frame size cap onOversizeFrame = "close" # close | drop onInvalidMessage = "close" # close | drop | hook maxBufferedBytes = 4M # outbound buffer cap before backpressure onBackpressure = "drop" # drop | close # maxConnections is unlimited by default; set a positive integer to cap. # Inbound frames held while the connection actor starts; past either cap # the socket is closed with 1013 instead of buffered without bound. maxPreAttachFrames = 256 maxPreAttachBytes = 4M # How long an admitted upgrade waits for its connection actor before the # socket is closed and its maxConnections slot released. Infinity to # disable (code only — HOCON has no Infinity literal). acceptTimeoutMs = 10s } }
cache { # Defaults for the built-in in-memory cache (the "default" cache, and any # cache whose plugin resolves to actor-ts.cache.in-memory). Leaf names # match the InMemoryCacheOptions fields (camelCase) and are validated on # read — a bad value throws OptionsError. in-memory { maxEntries = 10000 # LRU cap on entries (Infinity/unbounded only settable in code) cleanupMs = 60000 # background expired-entry sweep interval, ms (0 disables the sweep) } # Per-instance overrides live under the cache's own name and win over the # block above, so one consumer can be sized for its own key space: # # actor-ts.cache.idempotency.in-memory.maxEntries = 200000 # # The name is the application's, so these paths cannot be listed here. # actor-ts.cache.<name>.plugin selects the backend the same way. # # prefixQuotas splits ONE instance between the consumers writing into it, # so a flood of keys under one prefix evicts only that prefix's entries # (#607). Each quota is a cap and a reservation; they must sum to at most # that instance's maxEntries. Off by default — an undivided map is the # behaviour every release before it had. Quote the prefixes, they contain # a colon: # # actor-ts.cache.shared.in-memory { # maxEntries = 10000 # prefixQuotas { "rsp:" = 7000, "idem:" = 2000, "rl:" = 1000 } # } # # A per-name table replaces the global one rather than merging with it. }
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 # idle window before an entity passivates; 0 disables the sweep # shard-passivation-idle -- how long a shard may stand empty before it # stops as well. Deliberately left unset rather than given a value: # unset, it follows passivation-idle, which is what "the shard goes # when its entities do" needs. Set it (0ms disables) to decouple them. max-entities = 0 # 0 = no per-node cap }
worker-cluster { workers = "auto" # "auto" uses navigator.hardwareConcurrency restart-policy = "on-failure" # always | on-failure | never }
coordinated-shutdown { default-phase-timeout = 5s terminate-actor-system = true exit-process = false # call process.exit(0) once the pipeline completes
# Framework components register their own teardown in the pipeline: the # HTTP server unbinds in service-unbind, broker actors close their # connections in service-stop, a joined cluster leaves in cluster-leave, # DevTools detaches with the rest of the service layer. Set false to keep # the phases and register everything yourself -- for an embedder that owns # the lifecycle of the resources it handed the system. auto-register-tasks = true }}What is deliberately not here
Section titled “What is deliberately not here”Two families of settings have no entry above, and both are by design.
Plugin subtrees. Journals, snapshot stores, caches and brokers read
their own connection settings from their own subtree —
actor-ts.persistence.journal.postgres, actor-ts.io.broker.kafka,
actor-ts.cache.redis, and so on. They are optional peers, so shipping
defaults for them would mean shipping a configuration for a package you
may not have installed. Each is documented on its own page.
Anything that is not a value. The entity actor, message extractors,
allocation strategies, leases, transports, downing providers, custom
backends — a config file cannot express a class or a closure, so these
stay in code and are passed through the relevant XOptions builder.
Precedence
Section titled “Precedence”Three layers, highest first:
- Explicit options in code —
ClusterOptions.create().withPort(2552),ActorSystem.create('billing'). - Your
application.conf(or an inlineconfigobject). - This file.
Per field, not per block: setting one failure-detector threshold in code
leaves the other two coming from your config file. An undefined in code
means “not set” and falls through — it does not blank out the layer below.
No dead keys
Section titled “No dead keys”For a long stretch this file documented about twenty-five settings that
nothing in the framework ever read. You could set
actor-ts.sharding.passivation-idle = 2 minutes, get no passivation, and
receive no warning — the value was not rejected, it was never looked at
(issue #653).
That is now a build failure. A test walks every leaf above and asserts it
is both reachable from ConfigKeys — the typed list of paths the framework
recognises — and referenced from the source.
No key is exempt today. The list of deliberate exceptions still exists,
and every entry on it has to name the issue that will remove it — it is
simply empty. remote.tls.enabled was the last one, and
#591 emptied it by giving
the key a reader: it now decides whether the node warns at startup that the
cluster wire is plaintext. Note where that puts the bar — read, not
honoured. Encrypting the wire is still
#941; what this test rules
out is a key nothing so much as looks at.
