Chronix

Configuration Reference

Every Chronix and chronixd configuration key: defaults, units, and what each one trades away.

On this page

This page documents all configuration options for Chronix — both the embedded library (ChronixConfig) and the standalone server (chronixd).


Embedded Library — ChronixConfig

Build with ChronixConfig::builder():

use chronix::ChronixConfig;
use std::time::Duration;

let config = ChronixConfig::builder()
    .data_dir("/var/lib/chronix")
    .retention(Some(Duration::from_secs(30 * 86400)))
    .compression(CompressionCodec::Lz4)
    .build()?;

Core Settings

OptionTypeDefaultDescription
data_dirPathBufrequiredRoot directory for all data files
shard_durationDuration1 hourTime range per memtable shard
ooo_shard_toleranceu322Number of past shards that accept out-of-order writes. Writes outside the window use backfill() instead
future_write_toleranceDuration1 hHow far ahead of the wall clock a timestamp may be, on every path including backfill(). The out-of-order window is anchored on the newest admitted write, so without this bound one point from a device with a broken clock moved the anchor into the future and every real write after it was rejected until the next restart
retentionOption<Duration>None (keep forever)Global data retention period. The maintenance pass enforces every configured rule — this one, measurement_retention, and each rollup's own retention_ns — so per-measurement rules take effect whether or not a global one is set
measurement_retentionHashMap<String, Duration>emptyPer-measurement retention overrides
max_series_cardinalityusize1,000,000Maximum number of unique series
query_timeoutDuration60 sDeadline for one read. Checked before each time bucket's segment I/O, so a scan that runs out of time stops rather than finishing and then reporting. Duration::ZERO disables
per_query_memory_limitusize256 MiBMemory budget for one query's intermediate state
max_query_result_bytesusize256 MiBCeiling on the bytes one collected result may hold

WAL Settings

OptionTypeDefaultDescription
wal.fsync_policyFsyncPolicyPerBatchFsyncPolicy::PerWrite, PerBatch, or Periodic(Duration)
wal.fsync_intervalOption<Duration>NonePeriodic background fsync interval (e.g. 100ms). Enables start_periodic_sync()
wal.max_file_sizeusize32 MBMaximum WAL file size before rotation
wal.max_unflushed_walsusize4Backpressure: max WAL files before blocking writes

Memory & Storage

OptionTypeDefaultDescription
memtable_flush_thresholdusize64 MBFlush memtable when it exceeds this size
max_memtable_memoryusize256 MBTotal memory budget for all memtables
segment_cache_sizeusize512 MBLRU cache for decoded segment data
cdc_capacityusize65 536CDC events buffered for a slow subscriber. The ring (~112 bytes/event) is allocated on the first subscription, so a deployment that never reads the CDC stream pays nothing. open_small sets 4 096
enable_last_value_cacheboolfalseEnable O(1) last-value lookups
lvc_measurementsOption<HashSet<String>>NoneMeasurements to include in LVC (None = all)
field_encryptionFieldEncryptionemptyPer-column AES-256-GCM in the segment format — see Security

Encoding & Compression

OptionTypeDefaultDescription
compressionCompressionCodecLz4Block compression: None, Lz4, Zstd
float_encodingFloatEncodingChimpFloat encoding: Gorilla, Chimp

Compaction

OptionTypeDefaultDescription
compaction_concurrencyusize2Max concurrent compaction workers
maintenance_intervalDuration30 s (60 s in the small preset)How often the built-in maintenance thread compacts, materialises rollups, collects garbage and enforces retention; 0 disables the periodic passes (flush-on-demand stays)

Analytics

Two bounds, both enforced by the SQL forecast aggregates — and that is the whole section. The model, the detector, the confidence level and the anomaly threshold are arguments to the analytics API rather than settings, so a per-call choice stays a per-call choice; the section has no key for them and refuses a file that sets one.

OptionTypeDefaultDescription
analytics.max_forecast_horizonusize8760Most points one forecast() may predict. A call over it is a planning error naming this setting — refused rather than clamped, because a silently shortened forecast is a wrong answer
analytics.max_training_pointsusize1,000,000Most input points one series feeds a model; the newest are kept. 0 means no bound

Server — chronixd Configuration

The server is configured via a TOML file (default: chronixd.toml). It is a set of named sections: [server] holds the process-level scalars, [database] is the embedded database, and every other table switches a subsystem on by being present.

A key or a section the server does not know is a startup error naming it — not a warning, and not silence.

[server]
http_addr = "0.0.0.0:8086"
grpc_addr = "0.0.0.0:8087"
flight_addr = "0.0.0.0:8817"
log_format = "json"
log_level = "info"
max_body_size = 10485760

[database]
data_dir = "/var/lib/chronix"
compression = "lz4"
float_encoding = "chimp"
retention_secs = 2592000
segment_cache_size = 536870912
maintenance_interval_secs = 30
ooo_shard_tolerance = 2
future_write_tolerance_secs = 3600
soft_delete_ttl_secs = 86400

Database Settings

[database] maps to the embedded ChronixConfig, so everything in the Embedded Library tables above applies to a server deployment too. Each key below is optional; omitting it keeps the engine's default, which is why no second default is written here.

OptionTypeDefaultDescription
maintenance_interval_secsu64engine default (30 s)How often the maintenance thread compacts, materialises rollups, collects garbage and enforces retention. 0 disables the periodic passes; flush-on-demand stays. On flash-backed storage a longer interval is fewer writes
ooo_shard_toleranceu32engine default (2)Past shards that still accept live out-of-order writes. Anything older needs ?backfill=true
future_write_tolerance_secsu64engine default (1 h)How far ahead of the wall clock a timestamp may be, on every write path including backfill
cdc_capacityusizeengine default (65 536)Change events buffered for a slow subscriber. The ring is allocated on the first subscription, so a deployment that never reads the change stream pays nothing
wal_max_unflushedusizeengine defaultUnflushed WAL files tolerated before writes are held back
soft_delete_ttl_secsu64unset — drops are immediateGrace period before a dropped measurement is hard-deleted. With it set, DELETE /api/v1/measurements/{name} is recoverable until the deadline passes; without it the drop is irreversible. Measured from the wall clock capped by the newest timestamp held, as retention is, so a clock jump cannot close the window early
lvc_measurements[String]empty — all measurementsMeasurements the last-value cache covers, when enable_last_value_cache is on
field_encryption.columns{String: String}emptyColumn name → key id. Only a field may be encrypted; a tag or the time column is a write error. See Security
field_encryption.keys{String: String}emptyKey id → the name of an environment variable holding base64 of 32 bytes. Never the key itself. Every declared key must resolve at startup
checkpoints.interval_secsu64unset — offTake a checkpoint this often, from the maintenance thread. Requires checkpoints.directory
checkpoints.directoryPathBufunsetWhere scheduled checkpoints go. Must be outside the data directory's segments/ tree
checkpoints.keepusize3How many complete checkpoints to keep; older ones are removed after a successful run

Server Settings

OptionTypeDefaultDescription
http_addrSocketAddr0.0.0.0:8086HTTP API listen address
grpc_addrSocketAddrunset — port 8087 on http_addr's hostgRPC API listen address. Leave it unset and gRPC listens wherever HTTP does, so --bind 127.0.0.1:8086 keeps every listener on loopback; set it to put gRPC somewhere else
flight_addrSocketAddrunset — port 8817 on http_addr's hostArrow Flight SQL listen address, with the same rule as grpc_addr
authz_policy_dirOption<PathBuf>NoneDirectory of Cedar .cedar policies; absent = open mode
backup_rootOption<PathBuf><data_dir>/backupsRoot every admin backup/restore path is resolved inside
multi_tenancyboolfalseNamespace isolation on every read and write
metrics_pathString"/metrics"Prometheus metrics endpoint
max_body_sizeusize10 MBMaximum HTTP request body
log_formatenum"text"Log format: text, json, compact. A value that is none of these is a startup error, not plain text
log_levelString"info"Log filter: trace, debug, info, warn, error
sql_query_timeout_secsu6430SQL query timeout
prom_query_timeout_secsu6430PromQL query timeout (0 = disabled)
sql_max_rowsusize100,000Maximum rows per SQL result
max_write_batch_sizeusize50,000Maximum points per write request
cors_allowed_originsVec<String>emptyCORS allowed origins
authz_policy_dirOption<PathBuf>NoneDirectory containing Cedar authorization policy files. When set, the authorization engine loads .cedar policies from this path on startup

gRPC Settings

OptionTypeDefaultDescription
grpc_keepalive_secsu6460gRPC keepalive interval
grpc_keepalive_timeout_secsu6420gRPC keepalive timeout
stream_batch_sizeusize10,000Streaming batch size
stream_batch_interval_msu64100Streaming batch interval
dedup_window_secsu64300Deduplication window

TLS

[tls]
cert = "/etc/chronix/server.crt"
key = "/etc/chronix/server.key"
# Optional: CA certificate for mutual TLS (client cert verification)
client_ca = "/etc/chronix/ca.crt"
# Optional: hot-reload interval in seconds (0 = disabled)
# When enabled, the server watches cert/key files and atomically
# reloads TLS configuration on change — zero-downtime cert rotation.
reload_interval_secs = 3600
OptionTypeDefaultDescription
certPathBuf—Path to PEM-encoded server certificate
keyPathBuf—Path to PEM-encoded private key
client_caPathBuf?NoneCA cert for mutual TLS client verification
reload_interval_secsu6460Seconds between checks for a rotated cert/key; 0 disables. On by default — a certificate that expires without a reload is an outage with no warning

CLI equivalents:

chronixd --tls-cert /path/to/cert.pem \
         --tls-key /path/to/key.pem \
         --tls-client-ca /path/to/ca.pem \
         --tls-reload-interval-secs 3600

Authentication

[auth]
exempt_paths = ["/health", "/ready", "/metrics"]

[[auth.api_keys]]
name = "ingest"
key = "$CHRONIX_INGEST_KEY"      # env-var reference, Argon2 PHC string, or plain text
namespaces = ["tenant-a"]        # namespaces this key may act in; empty = all
admin = false                    # the administrative capability
roles = ["ingest-service"]       # Chronix::Role memberships, for Cedar policies

[auth.jwt]
secret = "your-jwt-secret"       # HS256/HS384/HS512 only
algorithm = "HS256"
issuer = "https://auth.example.com"     # optional
audience = "chronix"                    # optional
role_claim = "realm_access.roles"       # optional; defaults to "roles"

# For the asymmetric families, set a key file instead of a secret:
# algorithm = "RS256"                   # or PS*, ES256, ES384, EdDSA
# public_key_pem_file = "/etc/chronix/jwt-public.pem"
# jwks_url = "https://auth.example.com/.well-known/jwks.json"

An API key carries a principal name, the namespaces it may act in, whether it is an administrator, and its Cedar roles. A JWT carries the same four: namespaces (an array, or a single string), admin, and roles from the claim named by role_claim.

Roles are resolved once, at authentication, whichever credential was used, so the same role means the same thing at the data gate and the administrative gate — and principal in Chronix::Role::"…" matches on both. API keys carry roles as well as tokens.

namespaces is not optional under multi-tenancy. A key naming none is unrestricted and reads every tenant, so chronixd refuses to start with multi_tenancy = true while one exists, naming it in the error.

admin gates the administrative endpoints — restore, namespace management, key management, the log level — and it is required whether or not a policy directory is configured. With one, a policy permitting the route group's specific capability is required as well, so adding policies can only narrow what a credential reaches.

The verification key follows the algorithm's family. HS* reads secret; RS*, PS*, ES* and EdDSA read public_key_pem_file; jwks_url replaces the file for providers that publish and rotate their own keys, and is fetched once at startup. Pairing an algorithm with the other family's key material is a startup error.

Audit log

[audit]
path = "/var/log/chronix/audit.jsonl"
hmac_key_env = "CHRONIX_AUDIT_KEY"   # names the env var holding the key
sync_each = true                     # fsync after every event

Without this section the audit trail lives only in the process log, which does not survive a restart and cannot be shown to be unedited.

The hash chain continues across restarts, anchoring on the last sealed event in the file. Without the key it is a bare SHA-256, which detects corruption but not tampering. The key comes from the environment rather than the config, so reading the config does not confer the ability to forge history.

Backup root

[server]
backup_root = "/var/lib/chronix/backups"   # defaults to <data_dir>/backups

Every path the admin backup and restore endpoints accept is resolved inside this root, following symlinks. An absolute path outside it is refused.

Scheduled checkpoints

[database.checkpoints]
interval_secs = 21600                    # every six hours
directory     = "/mnt/backup/chronix"
keep          = 4

Off by default; taken by the maintenance thread. An interval without a directory is a startup error rather than a schedule that never runs. See Operations.

Field encryption

[database.field_encryption]
columns = { patient_id = "phi-2026", diagnosis = "phi-2026" }
keys    = { phi-2026 = "CHRONIX_FIELD_KEY_PHI" }

keys values are environment variable names, not keys — the file holds no key material. Each variable holds the base64 of 32 bytes, and every declared key must resolve at startup; a missing one stops the server with the variable named and the value never printed.

Only a field can be encrypted: declaring a tag or the time column is a write error. An encrypted column is refused by the Parquet export, the cold-tier archive and any rollup over its measurement. Full detail in Security.

Cluster

[cluster]
mode = "data"           # "meta" or "data"
node_id = 1
raft_bind_addr = "0.0.0.0:9000"
cluster_peers = ["node2:9000", "node3:9000"]
meta_addrs = ["meta1:9000", "meta2:9000", "meta3:9000"]

Connectors

[kafka]
brokers = "kafka1:9092,kafka2:9092"
group_id = "chronix-ingest"
topics = ["metrics"]
format = "line_protocol"

[mqtt]
broker = "mqtt.example.com"
port = 1883
client_id = "chronix-1"
topics = ["sensors/#"]
qos = 1
format = "json"
username = "chronix"                       # or credential_file, which rotates
password = "$CHRONIX_MQTT_PASSWORD"

A connector carries no request, so its namespace comes from its own configuration. Under multi_tenancy = true its points are tagged with that namespace, default included.

credential_file holds {"username": "...", "password": "..."} and takes precedence over the inline pair, so credentials rotate without a config change.

MQTT TLS is not available in this build — rumqttc's rustls feature pulls in an advisory-capped rustls-webpki and an unmaintained rustls-pemfile. Setting ca_cert, client_cert or client_key is refused at startup rather than ignored, so a connector never sends its password over a link the operator believes is encrypted. Terminate TLS in front of the broker.

Cold archiving

Absent by default, and requires a binary built with --features object-store; a [cold_archive] section without it is a startup error.

[cold_archive]
remote_url = "s3://bucket/chronix"   # or gs://, az://, file://
cold_after_secs = 2592000            # 30 days
interval_secs = 3600                 # one pass per hour
max_objects_per_run = 8

ArchiveOutcome.more_pending is true when the bound stopped a pass with work still eligible. The server's periodic task picks it up on the next tick; a caller driving archive_cold_segments itself should call again.

Each pass archives up to max_objects_per_run complete (measurement, shard) groups and stops; the next pass continues. A failed pass is logged and counted in chronix_cold_archive_pass_failures_total — not fatal, and the data stays hot and queryable. See Object Storage for what a group is and when it is eligible.

Signal triggers

Absent by default; the trigger endpoints answer 404 until it is present.

[triggers]
catalog_path = "triggers.json"          # relative to data_dir; survives restart
webhook_signing_secrets = ["$CHRONIX_WEBHOOK_SIGNING_SECRET"]
webhook_timeout_secs = 10
webhook_allow_private_targets = false   # see below
signal_store_capacity = 10000

webhook_signing_secrets is configuration rather than part of the DELIVER clause: a secret written in SQL lands in the trigger catalog on disk and in every SHOW TRIGGERS. With none set, DELIVER webhook(…) is refused at creation. Each entry may be a $VAR reference, resolved at startup.

It is a list because a delivery is signed with every entry, so a receiver holding any one of them verifies — see Rotating a signing secret. CHRONIX_WEBHOOK_SIGNING_SECRET sets the whole list, comma-separated.

webhook_allow_private_targets waives the resolved-address rule for an alerting endpoint on your own network — https://alertmanager.corp.example/ resolving to 10.x. It does not waive the https, userinfo or literal-address checks.

Observability

Log format and level are [server] settings; [tracing] only says where traces go.

[server]
log_format = "json"   # text | json | compact
log_level  = "info"

[tracing]
service_name = "chronix"

[tracing.otlp]
endpoint = "https://jaeger:4317"
sampling = { ratio = 0.01 }        # or "always_on" / "always_off"
always_sample_errors = true

OTLP export needs --features otlp. A [tracing.otlp] section in a build without it is a startup error, as is a [tracing] section naming no target.


Checking a file before a deploy

chronixd --config /etc/chronix/chronixd.toml --check-config

Loads the file, applies the environment and the flags, runs every startup validation, prints what it resolved to, and exits without opening the database or binding a port. An unknown key, a [tracing] section this build cannot honour, an [auth] section that authenticates nobody and two listeners on one address are all refused here.

configuration is valid
  http_addr        127.0.0.1:8086
  grpc_addr        127.0.0.1:8087
  flight_addr      127.0.0.1:8817
  data_dir         /var/lib/chronix
  log_format       Json
  log_level        info
  sql_max_rows     100000
  max_body_size    10485760
  multi_tenancy    false
  sections         tls, auth, audit

Environment Variables

Precedence is file, then environment, then CLI flags.

VariableOverride
CHRONIX_DATA_DIRdatabase.data_dir
CHRONIX_LOG_LEVELserver.log_level
CHRONIX_HTTP_ADDRserver.http_addr
CHRONIX_JWT_SECRETauth.jwt.secret — overrides an existing [auth.jwt] section; with no such section the server refuses to start rather than silently ignoring it
CHRONIX_WEBHOOK_SIGNING_SECRETtriggers.webhook_signing_secrets (comma-separated, newest first)

A value that cannot be parsed is a startup error, not a fall back to the default.

$VAR in secret-bearing settings

Separately from the table above, any field that holds a credential accepts a $VAR or ${VAR} reference resolved at startup — API keys (auth.api_keys[].key), and connector credentials (username, password, sasl_username, sasl_password). The variable name is yours to choose:

[mqtt]
broker = "mqtt://broker.example:1883"
topics = ["sensors/#"]
username = "chronix"
password = "$CHRONIX_MQTT_PASSWORD"

A referenced variable that is not set is a startup error for a connector, and a skipped key with a warning for an API key.

Improve this page on GitHub