Skip to main content

Service Configuration

This reference covers the configuration files and fields available for self-managed Director deployments. Managed Directors receive their configuration from the DataStream platform automatically; the fields documented here are relevant when you manage Director infrastructure directly.

For Director concepts and GUI-based setup, see Overview. For installation procedures, see Deployment.

The reference is split across three pages:

PageCovers
Service Configuration (this page)vmetric.yml — identity, network, queue, processing defaults, autoscale policy, persistent storage
Environment Filesconfig/environments/*.yml — topology, clustering, ACL, TLS, VIP
Health and LimitsHealth check thresholds and the hardcoded internal timing constants

Configuration Files​

A Director reads two categories of configuration at startup:

FileLocationPurpose
Service configurationvmetric.yml in the Director working directoryCore service settings (identity, network, queue, processing defaults)
Environment configurationconfig/environments/*.ymlTopology, clustering, ACL, and TLS

Supported formats: YAML (.yml / .yaml), TOML (.toml), and VMF (.vmf). Maximum file size: 1 MB.

Config file lookup order (first match wins):

  1. vmetric.toml
  2. vmetric.yml
  3. vmetric.yaml
Debug Mode

When the virtualmetric=debug environment variable is set, the service loads vmetric-local.yml (or .toml / .yaml) instead of the standard file. This is intended for local development and troubleshooting only -- do not use in production.

Minimal Standalone Director​

The smallest working service configuration: one Director, one listener port, console debug output.

vmetric.yml:

director:
id: 1
listen:
address: "0.0.0.0"
port: 8080
debug:
level: 1
console:
status: true

This file is not sufficient on its own -- a Director also needs an environment file declaring its topology, with a node name matching director.id. For the matching config/environments/env.yml, see Standalone Director in Environment Files.


Director Identity​

FieldYAML PathTypeDefaultDescription
IDdirector.idint64--Unique Director instance identifier
Statusdirector.statusbooltrueEnable or disable the Director service
Streamdirector.streamstring--NATS stream name for Director replies

Network​

FieldYAML PathTypeDefaultDescription
Addresslisten.addressstring0.0.0.0Bind address for all listeners
External Addresslisten.external_addressstring--Advertised address for remote connections (comma-separated for multiple)
Portlisten.portint8890Main collector listening port
WebSocket Portlisten.ws_portint9080VMMQ WebSocket port
HTTP Config Portlisten.httpconfigportint80 / 443HTTP configuration server port

Queue​

FieldYAML PathTypeDefaultDescription
Limitqueue.limitint172800 (48 h)Queue size limit in seconds
Parallelismqueue.parallelismintCPU countQueue processing parallelism
Non-Existing Target Limitqueue.non_existing_target_limitint86400 (24 h)TTL for queued data to non-existing targets (seconds)
Cleanup Intervalqueue.cleanup_intervalint900 (15 min)Cleanup interval for non-existing target queues (seconds)
DeQueue Intervalqueue.dequeue_intervalint1DeQueue processing interval in seconds

Production Standalone with ACL​

A single-node production Director: an advertised external_address for remote collectors, a raised queue limit, and file logging in place of console output.

vmetric.yml:

director:
id: 1
listen:
address: "0.0.0.0"
external_address: "10.0.1.100"
port: 8890
queue:
limit: 900000000
debug:
level: 1
console:
status: false
log:
status: true

The access control rules that accompany it are set in the environment file -- see Production Standalone with ACL in Environment Files.

Debug and Logging​

FieldYAML PathTypeDefaultDescription
Leveldebug.levelint0Debug verbosity: 0 = off, 1--3 = increasing detail
Console Statusdebug.console.statusboolfalseEnable console debug output
Log Statusdebug.log.statusboolfalseEnable file-based debug logging
Remotedebug.remoteboolfalseEnable remote debug mode (disables device tasks when true)

Fleet​

Fleet settings control how a Director connects to the DataStream cloud platform. Self-managed Directors that operate independently (no fleet connection) set self_managed: true and omit the fleet block.

FieldYAML PathTypeDefaultDescription
Self-Managedself_managedbool--If true, Director runs independently without fleet connection
Fleet Typefleet.typestring--Connection type (e.g., httpclient)
Fleet Instancefleet.instancestring--Fleet environment instance name
Fleet Addressfleet.addressstring--Fleet server WebSocket URL
Fleet Tokenfleet.tokenstring--Authentication token for fleet
Stats Drain Ratefleet.stats_drain_ratenumeric | string1048576Bytes per second the Director forwards its stats backlog to the platform at once the fleet link is up (1 MiB/s). Suffixed forms such as 4mb are accepted; values below 256 KiB/s are raised to 256 KiB/s. The platform may lower or pause the rate for a while; every such hint expires and the Director reverts to this value. See Stats Stream.

Fleet-Managed Director​

A Director that draws its configuration from the DataStream platform: self_managed: false with a populated fleet block. No environment file is required -- topology comes from the fleet.

vmetric.yml:

self_managed: false
fleet:
type: httpclient
instance: production
address: wss://fleet.virtualmetric.io/ws
token: "fleet-auth-token-abc123"
director:
id: 2028574672450228224
listen:
address: "0.0.0.0"
port: 8890
debug:
level: 1
console:
status: true

Data Processing Defaults​

These defaults apply to all data processing unless overridden at the device or target level.

FieldYAML PathTypeDefaultDescription
CompressioncompressionbooltrueEnable data compression
Compression Levelcompression_levelstring--Compression level (algorithm-specific)
ReusereusebooltrueEnable connection reuse
NormalizationnormalizationbooltrueEnable data normalization
Smart Data Enginesmart_data_enginebooltrueEnable SDxL processing
No Bufferno_bufferboolfalseDisable buffering
Batch Sizebatch_sizeint6410000Read batch size
Write Batch Sizewrite_batch_sizeint641000Write batch size
WorkersworkersintCPU countWorker count
Chunk Sizechunk_sizeint4194304 (4 MB)Data chunk size in bytes
Min Chunk Countmin_chunk_countint10Minimum chunk count
Stats Enabledstats_enabledboolfalseEnable per-device, per-route, and per-target metrics collection. Fleet-managed Directors override this to true automatically.
Stats Drop On Fullstats_drop_on_fullbooltrueWhen the stats channel buffer is full, drop new metrics rather than blocking the record producer.
Disable Directdisable_directboolfalseDisable the in-process listener-to-target fast path. When false, the direct path engages automatically when eligible.
Max Buffer Sizemax_buffer_sizeint | string128MBDefault ingest-buffer admission cap for memory-mode listener devices. Overridable per device. Sets each listener's ingest buffer size, which starts at an eighth of its ceiling and grows to 3x this value -- see Listener Memory Sizing before raising or lowering it.

Collector Resource Management​

Controls resource allocation and scheduling for device collectors.

FieldYAML PathTypeDefaultDescription
Retry Intervalretry_intervalint30Device retry delay in seconds
Start Delaystart_delayint1Collector startup delay in seconds
Windows Startwindows_startint524288000 (500 MB)Memory threshold for Windows collectors (bytes)
Windows Start Bufferwindows_start_bufferbool--Buffer mode for Windows collectors
Monitor Intervalmonitor_intervalint30Monitor interval in seconds
Flush Intervalflush_intervalint1Log flush interval in seconds
ModulesmodulesbooltrueEnable or disable module loading

Listener Memory Sizing​

A memory-mode listener's ingest buffers start at an eighth of their size and grow toward it only as the device's bursts need the room. A buffer keeps what it grew to until its device restarts. max_buffer_size sets that size and the burst admission cap that goes with it.

Set it per device under properties:, or service-wide as the top-level max_buffer_size shown under Data Processing Defaults. The device value wins.

A device takes max_buffer_size once, at its start. A change to a device's own value restarts that device, whatever its type, and a change to the service-wide value is taken by each device at its next start.

# vmetric.yml -- applies to every memory-mode listener
max_buffer_size: 32MB

devices:
- id: 1
name: syslog_edge
type: syslog
properties:
max_buffer_size: 16MB # this device only; overrides the service value

Accepted values​

ValueResult
A bare integerByte count
A size stringBinary units. 32MB and 32MiB are exact synonyms -- both mean 32 x 1024². Prefixes KB through PB (and KiB through PiB) are accepted
A positive value below 16MBRaised to the 16 MiB minimum, and logged. Below it a rotation cycle cannot hold a useful batch
0, a negative value, or an unparseable stringIgnored and logged; the service value, then the default, applies instead
warning

0 is not "unlimited". It would disable the admission cap and let the listener grow without bound, so it is refused. So is a negative or unparseable value -- and because those are discarded rather than rejected, the listener starts on the default with only a log line to record it. Remove the key to take the default deliberately.

Each listener's buffers can grow to three times this value​

The buffers split the cap across shards, gives each shard 1.5x its slice for encode headroom, and holds two VMFL slots per shard. The shard count cancels, so the multiple is 3x on any core count:

max_buffer_size / shards # shards = NumCPU/4, capped by GOMAXPROCS, clamped to [1,16]
x 1.5 # per-shard encode headroom
x 2 # two VMFL slots per shard
= 3 x max_buffer_size

That is the ceiling. The buffers start at an eighth of it, 3/8 of max_buffer_size: at the 128MB default, 48 MiB of live heap at start. A buffer doubles toward the ceiling only when a rotation's records need the room, three doublings at most, and keeps what it grew to until its device restarts. So a device that has burst once holds 384 MiB from then on. Size a Director by the ceiling for devices that will burst, and by the start for devices that never do.

The ceiling bounds what the buffers keep, not the heap while they grow. A doubling allocates the larger buffer before it releases the smaller one, which stays on the heap as garbage until the next collection. A device growing from its start to its ceiling leaves up to 7/8 of the ceiling behind that way, 336 MiB at the default, once, on its first burst. Under a memory limit, leave that much room above the ceiling for devices that will burst.

warning

The buffers are per listener worker, not per device. With reuse: true -- the default -- a device runs one listener per worker, each with its own buffers, so the real cost is workers x the per-listener figure, at start and at the ceiling. Multiply by the worker count before sizing a host.

DeviceDefault workersAt start / at the ceiling, at 128MB
syslog, tcpCPU cores / 8, uncapped8 workers on a 64-core host = 384 MiB, growing to 3 GiB per device
udp, http1 to 4 on a core-count ladder, capped at 44 workers = 192 MiB, growing to 1.5 GiB per device
Any device with reuse: falseForced to 148 MiB, growing to 384 MiB per device

A syslog device with enable_udp: true runs a second listener for the UDP side, with buffers of its own, doubling the figure again.

snmp-trap, smtp and tftp are the exception: each serves its port from one socket, so it always runs one listener and grows to 3x once per device, whatever workers and reuse say.

Growing costs a device little, and only once: a buffer's first fill copies what it holds at each doubling, and after that a rotation writes into it as it would into one allocated in full. Wire-speed throughput is unchanged.

max_buffer_sizeBuffers at start / grown, per listenerSuits
16MB (minimum)6 MiB / 48 MiBMany low-volume devices on one Director
32MB12 MiB / 96 MiBModerate volume
128MB (default)48 MiB / 384 MiBHigh-volume, wire-speed listeners

Under a cgroup memory or CPU limit the default already scales down on its own, to the smallest of 128MB, one quarter of the memory limit, and 32 MiB per CPU core of quota, floored at the 16 MiB minimum. It is computed once at startup. Bare-metal and virtual-machine hosts get no such scaling, so a device there can reach the full size, which is what this key is for.

note

In earlier versions, a memory-mode listener allocated its buffers in full when its device started: 384 MiB of live heap at the default before the first record arrived. That heap counted against the Go memory limit, which the Director sets to 90% of a cgroup memory limit. Eight idle devices in a 4 GiB container held 3.3 GB of heap; they now hold about 0.47 GB.

note

Under a cgroup memory limit, a second cap bounds the bytes admitted across all listeners together at one quarter of that limit. Raising max_buffer_size on one device does not necessarily buy it more admission.

What happens when the cap is exhausted​

The cap is also the burst admission gate, so lowering it shortens how long a burst may outrun the router. What that costs depends on the protocol:

DeviceBehavior
syslog over TCP, tcpGraceful. The read loop blocks, the TCP window closes, and senders slow down
syslog with UDP, udp, the flow listenersNo backpressure. The read loop stalls and the kernel discards datagrams
httpThe request waits up to 30 seconds, then returns 503 with Retry-After: 30
splunk-hecSame, plus HEC status code 9 (server busy). A batch that times out part-way leaves the already-encoded prefix stored and returns 503 for the whole request, so a client retry duplicates that prefix
otlpWaits up to 30 seconds, then answers per transport: 503 over OTLP/HTTP, gRPC status UNAVAILABLE over OTLP/gRPC
wecNo backpressure and no rejection. The SOAP endpoint acknowledges the batch before the write outcome is known, so an event that cannot be admitted is dropped with only a log entry
warning

Size UDP and wec listeners conservatively -- they are the two that lose data rather than slowing the sender when the cap is exhausted. UDP has no flow control to fall back on, and wec has already acknowledged the batch by the time the write is attempted. A syslog device with enable_udp: true carries the UDP exposure even when network is tcp.

When a payload cannot be handed on​

Every rotation, every 250 ms, a listener hands its buffered records on as one payload: into the payload store, with a notification published over NATS. When the store refuses the payload, or the notification cannot be published -- NATS is out of reach, or the cluster is joining -- the payload stays in its slot and is handed on again at every rotation until it goes through. Nothing written after it is handed on before it.

SituationResult
Payload store or NATS unavailableThe payload is kept and retried every 250 ms. Its bytes stay counted against max_buffer_size, so once the rest of the buffer fills, the listener behaves as it does when the cap is exhausted (see What happens when the cap is exhausted): TCP senders are slowed, http and splunk-hec answer 503 after 30 seconds, otlp answers 503 over OTLP/HTTP and UNAVAILABLE over OTLP/gRPC after 30 seconds, and UDP, the flow listeners and wec drop records
The failure clearsThe payload is handed on, and everything written after it follows. Records are acknowledged once they are delivered
Still failing when the collector stops -- the device is restarted or reconfigured, or the Director is stoppingThe payload is given up. Its records are dropped, and neither they nor any record written after them are acknowledged. A source that acknowledges after delivery (kafka, nats, rabbitmq, MQTT, Pub/Sub, event-hubs, the SQS-driven listeners, the file device) sends them again: duplicates at worst, never silent loss. With crash_resistance the queue file stays for the next start
A payload no attempt can hand on -- larger than the store accepts, or its queue file deletedA listener that acknowledges nothing (syslog, tcp, udp, http, otlp, and so on) drops it at once and goes on. A listener that acknowledges holds it: its device stops ingesting until it is restarted, when the payload is given up unacknowledged and the source sends it again

This applies to every device in the first two rows of Listeners this applies to: every listener whose records go through the Director's ingest stream.

warning

A long outage of NATS or the payload store shows up as listeners that stop taking data, not as data that quietly disappears. UDP-fed and wec listeners still lose records once their buffer is full, as they always have.

note

With persistent_storage.payload.type: inline, a rotation larger than persistent_storage.payload.max_size (1 MB by default) is one the store never accepts: a listener shard taking more than about 4 MiB/s, 1 MB per 250 ms rotation, produces them. Raising persistent_storage.payload.max_size avoids it, in step with the bus's message limit. See Persistent Storage.

The Director logs each of these cases. The lines are listed under Common Log Messages in Troubleshooting.

Interaction with crash resistance​

The buffers apply only in memory mode. With persistent_storage.crash_resistance enabled the listener takes the disk-mode path and holds no buffers in memory, so the key narrows admission but frees nothing. See Persistent Storage.

note

Memory mode is a property of the Director, not of a device: crash_resistance is a service-level flag, so a Director is either entirely memory-mode or entirely disk-mode. There is no per-device escape.

Listeners this applies to​

Device types
Grow to 3x and honor the capsyslog, tcp, udp, http, otlp, splunk-hec, wec, snmp-trap, smtp, tftp, netflow, sflow, ipfix, elastic, file, every honeypot device, the message-queue devices (kafka, nats, rabbitmq, amqp, redis), and the cloud pull devices (amazon-s3, azure-blob-storage, event-hubs, google-cloud-pub-sub). rabbitmq and amqp write through one ingest stream per worker and hold their buffers once per worker: from 4 x 48 MiB up to 4 x 384 MiB = 1.5 GiB at the defaults of 4 workers and 128MB. snmp-trap, smtp and tftp run one listener and grow to 3x once per device, whatever workers and reuse say
Honor the cap, reserve nothingestreamer, ccf, amazon-security-lake, google-cloud-storage, and the database devices (mssql, mysql, oracle, postgres)
Ignore the key entirelydatastream-stats, proofpoint-on-demand, microsoft-graph-api, datagen, azure-monitor, microsoft-sentinel, and the linux and windows agent devices

A syslog or tcp device also builds no buffers when it has no routes, or when every route it feeds targets only discard.

Profiler​

FieldYAML PathTypeDefaultDescription
Profile Portprofiler.director_profile_portint6061pprof profiler port
Timeoutprofiler.timeoutint60Profiler and metrics collection timeout in seconds

Load Balancer​

FieldYAML PathTypeDefaultDescription
Modeloadbalancer.modestring--Set to dynamic for dynamic load balancing across cluster nodes

Three-Node HA Cluster​

loadbalancer.mode: dynamic spreads work across the nodes of a cluster. The same vmetric.yml goes on all three nodes -- only director.id differs.

vmetric.yml:

director:
id: 1 # Change to 2, 3 on other nodes
listen:
address: "0.0.0.0"
external_address: "10.0.1.101"
port: 8890
queue:
limit: 900000000
debug:
level: 2
console:
status: true
log:
status: true
loadbalancer:
mode: dynamic

The cluster topology itself -- node addresses, inter-node TLS, ACL, and VIP failover -- is declared in the environment file. See Three-Node HA Cluster with VIP in Environment Files.

Autoscale Policy​

Decides how many replicas a Serverless or Orchestrated Director asks its platform's autoscaler for. The Director publishes the result on its /scale endpoint, on the same HTTPS port as /status, and a scale rule polls it. The Azure Container Apps deployment manifest ships such a rule already configured -- see The Azure Container Apps scale rule in Deployment.

These keys are node properties, delivered under properties.autoscale on every node of the Director. On the platform you set them through the Autoscaling policy fields of a serverless Director -- see General Settings in Deployment. A key you never set is omitted rather than written with its default, so the Director follows its own default for it from release to release. The block is re-read on the Director's refresh loop, so a change retunes a running replica set without a redeploy.

FieldYAML PathTypeDefaultDescription
Backlog per Replicaproperties.autoscale.backlog_per_replicaint10000Queued records one replica is expected to hold. 0 or less stops backlog counting toward the decision
CPU Targetproperties.autoscale.cpu_target_percentint75CPU utilization to hold each replica at, in percent. 0 or less stops CPU counting toward the decision
Memory Targetproperties.autoscale.memory_target_percentint0Memory utilization to hold each replica at, in percent. 0 (the default) leaves memory unweighed
Max Replicasproperties.autoscale.max_replicasint0Ceiling on the replica count asked for. 0 means no ceiling, never "no replicas"
Sample Intervalproperties.autoscale.sample_intervalint5Seconds between a replica publishing its own sample to the rest of the replica set. 0 or less falls back to the default. Not exposed in the web interface; leave it at its default
On Kubernetes

A serverless Director on Kubernetes is scaled by a HorizontalPodAutoscaler that the platform renders into its manifest, not by a scale rule polling /scale. Only three keys have a meaning there:

KeyOn Kubernetes
cpu_target_percentThe HPA's CPU averageUtilization (default 75). 0 leaves the CPU metric out
memory_target_percentThe HPA's memory averageUtilization. 0, the default, leaves the memory metric out
max_replicasThe HPA's maxReplicas. 0 or unset gives the manifest's default of 10, rather than no ceiling
backlog_per_replica, sample_intervalInert. They still reach the Director, but nothing polls /scale on Kubernetes

With both targets at 0 no HPA is rendered. Because the thresholds live in the manifest, a change takes a re-apply of the reinstalled manifest rather than reaching the running replica set on the refresh loop. See Autoscaling on Kubernetes in Deployment.

What the endpoint reports​

A three-replica set running the default policy might report:

{"pressure": 482, "demand": 482, "members": 3,
"backlog": {"total": 48213, "local": 15804, "unacked": 9000, "max": 17102},
"cpu_percent": 71.2, "memory_percent": 63.8,
"stale_members": 0, "coordinated": true, "coordination": "ok"}

pressure is 100 times the desired replica count, rounded to the nearest whole number, so a scale rule with a fixed target value of 100 and the usual ceil(current / target) arithmetic lands on the count computed here. Every other field is there so a scaling decision can be explained afterwards. Three replica counts are computed and the highest wins:

backlog desired = backlog.total / backlog_per_replica
cpu desired = members × cpu_percent / cpu_target_percent
memory desired = members × memory_percent / memory_target_percent

In the example above, backlog asks for 48213 / 10000 = 4.82 replicas, CPU for 3 × 71.2 / 75 = 2.85, and memory is not weighed because its target is 0. Backlog wins, pressure is 482, and a scale rule targeting 100 rounds that up to five replicas. demand equals pressure because no max_replicas ceiling is in force; with one, demand keeps reporting what the inputs asked for while pressure is capped -- see Edges and precedence.

The figure is a replica-set aggregate, not one replica's reading: every replica publishes its own sample, and any replica answers with the same total. That is what makes the endpoint safe to poll through a load balancer, which resolves one URL to whichever replica it chooses. A replica set whose members cannot share samples answers from the local sample alone, in the same shape, with coordinated reporting false and coordination saying why.

Edges and precedence​

  • Pressure never falls below 100. A Director is a collector, not a request handler: at zero replicas its listeners are closed and nothing arrives to wake it, so the policy never asks an autoscaler to remove the last replica.
  • Zero does not mean zero. On cpu_target_percent and memory_target_percent, 0 means do not weigh this input, not "zero percent". On max_replicas, 0 means no ceiling, never "no replicas".
  • Memory is off by default, deliberately. An idle Director sits close to a quarter of its memory allocation and does not release memory promptly once a burst has passed, so a memory target would drive every scaling decision and hold replicas up long after the work is gone. Backlog describes work waiting and CPU describes work in progress; memory describes neither. Enable it only where memory is the constraint you actually scale on.
  • max_replicas can only reduce. The hosting platform's own maximum still applies on top -- the Azure Container Apps manifest sets it to 10 -- so the smaller of the two wins and a value above the platform maximum changes nothing. While a ceiling is in force the endpoint reports both pressure (the capped figure the scaler reads) and demand (what the inputs actually asked for), plus max_replicas, so a replica set pinned at its ceiling can be told apart from a calm one.
  • Scaling out does not drain an existing backlog. A new replica receives only new arrivals; records already queued on another replica stay there. Scaling out relieves incoming pressure, not a queue that has already built up. When one replica holds 90% or more of the total backlog and the set already has more than one replica, backlog.concentrated becomes true and the backlog demand is held at the current replica count rather than grown -- more replicas would sit idle while the backlog that triggered them does not move. This arises with raw TCP listener ports, where a connection stays pinned to the replica that accepted it, so one high-volume source is one stream into one replica; it is a vertical-scaling problem, not a horizontal one.
  • Access control applies. /scale sits behind the same ACL as /status: open while no allowed IPs and no access tokens are configured, enforced the moment either is. An autoscaler must satisfy it like any other client. The Azure Container Apps manifest renders the Director's access token into the scale rule when the Director has one -- but only once, at create or reinstall. A token rotated later, or added to a Director that previously had none, never reaches the deployed rule, which is then refused with no symptom except a replica count that stops moving. Reinstall the Director to re-render it.

backlog_per_replica set too low scales out on noise and bills for replicas that have nothing to do; set too high, it lets a backlog build before anything reacts. Neither loses data on its own -- the ceiling on how much backlog can exist at all is the sender stream's byte limit, not this key.

Persistent Storage​

Controls whether pipeline data survives a restart, and how far upstream that durability extends. The two flags combine into four modes:

persistent_storage.statuscrash_resistanceEffect
false (default, standalone)forced falseIn-memory VMFL/VMF and JetStream MemoryStorage for the pipeline streams. Maximum throughput; records buffered in process memory are lost on a crash.
truefalseJetStream FileStorage plus the NATS Object Store for payloads. VMFL/VMF writers stay in-memory; durability is provided by NATS.
truetrueEnterprise: local VMFL/VMF queue writers also go to disk, flushing each record to the OS page cache -- process-crash durable end-to-end.
falsetrueIgnored -- crash_resistance is forced false and the mode collapses to the first row. The override is silent: no warning is logged.
note

The first row applies to the three pipeline streams (ingester, router, sender). The two fleet control streams, the Director data stream and the stats stream are created with FileStorage regardless of persistent_storage.status, so some disk is reserved in every mode.

FieldYAML PathTypeDefaultDescription
Statuspersistent_storage.statusboolfalseEnable the durable NATS JetStream pipeline layer (JetStream FileStorage + NATS Object Store). Defaults to true on fleet-managed Directors.
Crash Resistancepersistent_storage.crash_resistanceboolfalseAlso write the local VMFL/VMF queue files to disk, flushing each record to the OS page cache so the upstream pipeline hops survive a process crash (SIGKILL, OOM, panic). Enterprise deployments; only effective when persistent_storage.status: true (forced false otherwise).
Max Agepersistent_storage.max_agenumeric172800Retention horizon in seconds for the durable streams (48 hours). A non-positive value falls back to the default.
Max Bytespersistent_storage.max_bytesnumeric | string--Byte cap of each of the three pipeline streams (ingester, router, sender) that has no positive key of its own under persistent_storage.streams. Per stream, not a total divided among them. Suffixed forms such as 512mb are accepted. Unset or non-positive, each stream takes its own default. In file mode each stream's resolved cap -- its own key, else this value, else its default -- is reserved on disk up front, so the three together have to fit in the NATS store. The streams' file-mode defaults are scaled down to fit a detected disk budget (see VMETRIC_STORE_BUDGET_BYTES under Environment Variables); a value set here is never scaled.
Sender Stream Sizepersistent_storage.streams.sendernumeric | string1073741824 (persistent), 536870912 (memory)Byte cap of the sender stream, the stage that holds a backlog while a target is down: 1 GB with persistent_storage.status: true, 512 MB without. Wins over persistent_storage.max_bytes for this stream. Suffixed forms are accepted. A non-positive value falls back to max_bytes, then to the default.
Router Stream Sizepersistent_storage.streams.routernumeric | string536870912 (persistent), 268435456 (memory)Byte cap of the router stream: 512 MB with persistent_storage.status: true, 256 MB without. Wins over persistent_storage.max_bytes for this stream. Same fallback as the sender stream.
Ingester Stream Sizepersistent_storage.streams.ingesternumeric | string536870912 (persistent), 268435456 (memory)Byte cap of the ingester stream: 512 MB with persistent_storage.status: true, 256 MB without. Wins over persistent_storage.max_bytes for this stream. Same fallback as the sender stream.
Stats Stream Sizepersistent_storage.streams.statsnumeric | string536870912Byte cap of the stats stream (512 MB), in both storage modes. Suffixed forms such as 1gb are accepted. Not sized by persistent_storage.max_bytes. A non-positive value falls back to the default. See Stats Stream.
Stats Stream Max Agepersistent_storage.streams.stats_max_agenumeric | string86400How long a stats batch stays on the stats stream (24 hours), as a duration (6h, 1d) or a number of seconds. A non-positive value falls back to the default.
note

crash_resistance also decides how listeners allocate their ingest buffers: memory mode holds them in memory, starting at an eighth of their size and growing, while disk mode does not. See Listener Memory Sizing.

note

crash_resistance targets process-crash durability, not power loss. With persistent_storage.status: true, power-loss durability is provided by NATS -- best-effort on a single node (default 2-minute sync interval) or per-write via RAFT quorum on a cluster. For power-loss-sensitive workloads, run a cluster.

In clustered or environment-file deployments, place the persistent_storage block -- including its nested payload and pipeline_bus -- under a node's or the cluster's properties: block rather than at the top level of vmetric.yml. See Embedded NATS below, and External Kafka or Redpanda and Azure Event Hubs under Pipeline Bus.

Cluster properties are applied first and node properties are applied on top, so a value set on a node overrides the cluster value for that node. Nothing validates consistency across a cluster: divergent values are accepted silently and each node runs with its own resolved flags. Keep them uniform -- nodes of a cluster share the same NATS and pipeline layer, and mixed durability settings produce behavior that is difficult to reason about.

The web interface also sets the processing mode, the external storage connection of Orchestrated and Serverless Directors, and three persistent_storage keys: max_age (Retention), max_bytes (Queue size limit, not offered on Orchestrated Directors) and payload.max_size (Inline payload size, applied only where a broker is the pipeline bus) -- see Persistent Storage.

Stats Stream​

Every Director keeps its own stats on a file-backed stream before forwarding them to the platform and to any stats device. Stats feed usage accounting, so the stream's retention is the loss budget for a platform outage: a batch the platform link could not carry within persistent_storage.streams.stats_max_age (24 hours by default) is gone. Agents do not have this stream.

ConfigResult
persistent_storage.streams.stats: 1gb1 GiB reserved on disk
persistent_storage.streams.stats: 0 or negativeFalls back to the default -- never "unbounded"
Nothing set512 MB on disk, in both storage modes
The server refuses the reservationThe Director halves the cap down to a 64 MB floor and logs one Warning naming the requested and the granted figure. Below the floor the stream fails to start.

512 MB over 24 hours is about 6 KiB/s of stats. A larger Director reaches the byte cap before the age limit, and logs a Warning once per run when it finds the stream at its cap while the oldest batch is younger than the retention. The fix is to raise persistent_storage.streams.stats, not stats_max_age.

Once the fleet link is up, the Director forwards its backlog at a paced rate, fleet.stats_drain_rate (1 MiB/s by default), so it never floods the platform after an outage. The log says once per outage when forwarding paused and, when the link returns, how long the outage lasted and how many batches expired from retention in the meantime. A stats device created later replays up to the stats_max_age window on its first start.

Disk reservation. A stream's byte cap is a hard reservation taken at startup, accounted against 75% of the free space on the NATS store directory on Linux and macOS. With the defaults, a Director in persistent mode reserves 4.50 GB across seven streams, needing about 6 GB free. In memory mode the always-file-backed streams (fleet control pair, data stream and stats stream) reserve 2.5 GB, needing about 3.33 GB free. In a cluster, multiply by the replica count. The Director logs its total at startup -- check it after raising any stream size.

Azure Container Apps. Free space is not the bound here: the container sees the node's disk, while the replica has an ephemeral-storage quota that Azure enforces by evicting it (1 GiB at 0.25 vCPU, 2 GiB at 0.5, 4 GiB at 1, 8 GiB above 1). The Director detects the platform and its vCPU quota, takes the tier minus 1 GiB of headroom as its disk budget (on the 0.25 and 0.5 vCPU tiers, half the tier), and scales the shipped defaults of the file-backed streams down by one factor so their total fits, keeping their ratios. A 1 vCPU replica in persistent mode gets about two thirds of each default. A stream size you set is never scaled. The startup line names the budget and the factor whenever scaling applied. VMETRIC_STORE_BUDGET_BYTES pins the budget on any platform -- see Environment Variables.

note

Upgrading. Earlier versions kept the stats stream in memory unless persistent_storage.status was on, with a 64 MB cap. After an upgrade, with the default size unscaled, a Director in persistent mode reserves an extra 448 MB of disk at startup (512 MB instead of 64 MB), and one in memory mode an extra 512 MB, since its old stream took no disk at all. On an embedded single-node VMMQ the old in-memory stream disappears with the process, so the restart is clean. In a cluster the surviving in-memory stream is deleted and recreated file-backed once, losing at most the old five-minute window.

Embedded NATS​

Durable pipeline on the embedded NATS backend -- the default when no payload or pipeline_bus is set. JetStream switches to FileStorage and payloads move to the NATS Object Store; VMFL/VMF writers stay in-memory. In a cluster, put these settings on the cluster's properties (they must be uniform across all nodes), not per-node.

config/environments/env.yml:

environments:
- name: "1"
status: true
nodes:
- name: "1"
status: true
properties:
persistent_storage:
status: true

For the Enterprise crash-resistant mode, add crash_resistance: true -- the local VMFL/VMF queue writers also go to disk, flushed per record:

persistent_storage:
status: true
crash_resistance: true

Payload​

Configures the payload backend that carries log record bytes between pipeline stages. The payload and pipeline_bus blocks nest inside persistent_storage; the full key paths are persistent_storage.payload.* and persistent_storage.pipeline_bus.*.

FieldYAML PathTypeDefaultDescription
Typepersistent_storage.payload.typestring""Payload backend: inline (bytes carried in the pipeline message), object_store (NATS Object Store), memory (in-process store), or "" (automatic — memory when persistent storage is off, object_store when on).
Max Sizepersistent_storage.payload.max_sizestring""Maximum payload size per record. Accepts a plain integer (bytes) or a size suffix such as 1mb or 512kb. When the payload type is inline defaults to 1 MB to fit within Kafka and Event Hubs message limits; unlimited for other backends.

Pipeline Bus​

Configures the transport that moves stage-to-stage pipeline notifications between the ingester, router, and sender workers.

FieldYAML PathTypeDefaultDescription
Typepersistent_storage.pipeline_bus.typestringnatsStage-to-stage transport: nats (embedded VMMQ, default), kafka (external Kafka broker), or eventhubs (Azure Event Hubs, treated as kafka against its Kafka-compatible endpoint). An unrecognized value logs a warning and falls back to nats.
Addresspersistent_storage.pipeline_bus.addressstring--Comma-separated Kafka broker addresses. Required when the pipeline bus type is kafka.
Topic Prefixpersistent_storage.pipeline_bus.topic_prefixstringvm-pipelinePrefix for stage topics. Topics are named <prefix>.<stage> (e.g., vm-pipeline.sender).
Algorithmpersistent_storage.pipeline_bus.algorithmstring--Kafka SASL algorithm: plain, scram-sha-256, scram-sha-512, or gssapi.
Usernamepersistent_storage.pipeline_bus.usernamestring--Kafka SASL username.
Passwordpersistent_storage.pipeline_bus.passwordstring--Kafka SASL password.
TLS Statuspersistent_storage.pipeline_bus.tls.statusboolfalseEnable TLS for the Kafka broker connection.
TLS Insecure Skip Verifypersistent_storage.pipeline_bus.tls.insecure_skip_verifyboolfalseSkip broker certificate verification.
TLS Min Versionpersistent_storage.pipeline_bus.tls.min_tls_versionstringtls1.2Minimum TLS version for the broker connection.
TLS Max Versionpersistent_storage.pipeline_bus.tls.max_tls_versionstringtls1.3Maximum TLS version for the broker connection.
note

pipeline_bus.type: kafka requires payload.type: inline. If payload.type is not inline, the Director logs a warning and reverts to the nats transport.

External Kafka or Redpanda​

Routes the router-to-sender hop over an external Kafka-compatible broker. Requires payload.type: inline -- with any other payload type the bus silently falls back to nats. The topic (vm-pipeline.sender) and consumer group (<director-name>-sender) auto-create where the broker allows it.

config/environments/env.yml:

environments:
- name: "1"
status: true
nodes:
- name: "1"
status: true
properties:
persistent_storage:
status: true
payload:
type: inline
max_size: 512kb
pipeline_bus:
type: kafka
address: "{broker-1}:9092,{broker-2}:9092"
topic_prefix: vm-pipeline
# Optional SASL auth -- omit the block below for a plaintext broker.
# algorithm: plain # plain | scram-sha-256 | scram-sha-512 | gssapi
# username: "{username}"
# password: "{password}"
# tls:
# status: true

The inline payload rides base64-encoded inside the bus message, so max_size caps the raw payload before encoding (~4/3 on the wire). 512kb (~683 KB encoded) stays under a default broker's ~1 MB message.max.bytes; raise it only in step with the broker limit. For crash resistance, add crash_resistance: true under persistent_storage (the shipped example also raises max_size to 1mb).

Azure Event Hubs​

Uses the Event Hubs Kafka endpoint as the pipeline bus -- the durable-broker option for ephemeral deployments (e.g. Azure Container Apps) with no durable local disk, where in-flight data survives a container restart inside the managed broker. SASL PLAIN over TLS on port 9093.

config/environments/env.yml:

environments:
- name: "{director-id}"
status: true
nodes:
- name: "{director-id}"
status: true
properties:
persistent_storage:
status: true
payload:
type: inline
max_size: 512kb
pipeline_bus:
type: kafka
address: "{namespace}.servicebus.windows.net:9093"
topic_prefix: vm-pipeline
algorithm: plain
username: "$ConnectionString"
password: "Endpoint=sb://{namespace}.servicebus.windows.net/;SharedAccessKeyName={policy};SharedAccessKey={key}"
tls:
status: true

Event Hubs does not auto-create topics: pre-create the event hub vm-pipeline.sender and the consumer group <director-name>-sender before starting the Director -- a missing topic drops 100% of pipeline traffic. Keep payload.max_size at or below the namespace tier's message cap (Basic 256 KB, Standard/Premium 1 MB, Dedicated 20 MB), measured on the raw payload before base64. Add crash_resistance: true under persistent_storage for the Enterprise crash-resistant mode.


Environment Variables​

VariableValueEffect
virtualmetricdebugLoads vmetric-local.yml instead of vmetric.yml
VMETRIC_STORE_BUDGET_BYTESbytes, or offPins the disk budget that the shipped defaults of the file-backed stream sizes are scaled to fit, on any platform. Set it on a Kubernetes pod, whose ephemeral-storage limit is not visible from inside the container. off disables the automatic Azure Container Apps detection. Stream sizes set under persistent_storage.streams are never scaled. See Stats Stream.

Debug config file lookup order:

  1. {serviceName}-local.toml
  2. {serviceName}-local.yml
  3. {serviceName}-local.yaml