Configuration

Everything configurable in Ursula lives on this page. Ursula is configured by a TOML config file, an optional resource preset, and a small set of CLI overrides, merged in this order:

built-in defaults < preset defaults < config file < CLI overrides

The normal startup command is:

ursula --config /etc/ursula/ursula.toml --node-id 1

With no config file at all, ursula starts a single-node in-memory runtime. That's what Install uses.

CLI flags

FlagDefaultNotes
--config FILEsearches ./ursula.toml, /etc/ursula/ursula.toml, then user config pathsTOML config file
--preset PRESETdefault when no config file is found, otherwise noneResource preset: default, dev, tiny, small, standard, or large
--node-id IDconfig file or presetOverrides raft.node_id

CLI --node-id intentionally overrides any raft.node_id value in the config file so the same file can be mounted on every node. Helm derives it from the StatefulSet ordinal.

Resource presets

--preset supplies a base configuration before the config file is merged, and config-file values win. Pick by node size:

PresetIntended useMain defaults
default, devLocal developmentSingle-node in-memory runtime
tinyMemory-bound tests or very small nodes64 groups, 64 MiB cold cache, 4 MiB cold flushes, 2 cold writes, 8 MiB hot/admission caps, 64 MiB HTTP in-flight body budget
smallSmall nodes or cost-sensitive tests128 groups, 64 MiB cold cache, 4 MiB cold flushes, 2 cold writes, 16 MiB hot/admission caps, 64 MiB HTTP in-flight body budget
standardProduction baseline256 groups, 256 MiB cold cache, 8 MiB cold flushes, 4 cold writes, 64 MiB hot/admission caps, 256 MiB HTTP in-flight body budget
largeLarger nodes with more cache and write headroom512 groups, 512 MiB cold cache, 16 MiB cold flushes, 8 cold writes, 128 MiB hot/admission caps, 512 MiB HTTP in-flight body budget

Server and runtime

Config keyDefaultPurpose
server.listen127.0.0.1:4437Client/API listener
server.cluster_listennoneOptional separate Raft/cluster listener
runtime.core_countavailable_parallelismWorker threads, each pinned to its mailbox event loop
runtime.live_read_max_waiters_per_core65536SSE waiter cap per core. 0 disables the limit
observability.tokio_consolefalseEnables Tokio console when the binary is built with the feature

Without server.cluster_listen, server.listen serves both client/API and Raft/cluster routes, and each [[raft.peers]].url should point at that address. With server.cluster_listen set, server.listen is client/API-only and peer URLs must point at the cluster listener instead. Note that the same peer URL is currently also used for HTTP leader redirects, so it must be reachable by clients or the gateway as well as by peers.

Raft

Config keyDefaultPurpose
raft.node_idnoneThis node's stable ID, usually overridden per host with --node-id
raft.group_countcore_count × 16Total Raft groups. Higher values improve stream-to-group hash spread
raft.wal.backendmemorymemory (volatile, survives no restart) or disk (durable, required for clusters)
raft.wal.pathnoneRequired for backend = "disk". Raft logs live under PATH/raft-log, and removing the directory wipes state cleanly
raft.peersemptyStatic gRPC peer list, with one [[raft.peers]] entry per voter, including this node
raft.init_membership_per_groupfalseOne-time per-group membership bootstrap. Set true on the very first start of a fresh cluster, then flip back to false

Pick one WAL backend per cluster and use it on every peer.

Cold storage

Ursula keeps recent data in an in-memory hot ring on every replica and flushes older segments and snapshot blobs to a cold backend. Multi-node clusters should use S3 or an S3-compatible object store so every replica can read chunks flushed by any leader. All replicas must point at the same shared bucket.

[storage.cold]
backend = "s3"
root = "ursula-prod-20260518"

[storage.cold.s3]
bucket = "my-ursula-bucket"
region = "us-east-1"
# endpoint = "http://127.0.0.1:9000"
# access_key_id = "AKIA..."
# secret_access_key = "..."
# session_token = "..."
# server_side_encryption = "aes256"   # default; "aws-kms" or "none"
# kms_key_id = "arn:aws:kms:..."      # customer managed key for "aws-kms"

When access_key_id and secret_access_key are omitted, Ursula uses the standard AWS SDK credential chain (instance profile, environment, profile, and so on). Prefer that over static keys. Set endpoint for S3-compatible stores such as MinIO, R2, or TOS.

Every cold-tier object write requests server-side encryption by default (server_side_encryption = "aes256", i.e. SSE-S3) — free on AWS S3, and Raft snapshot uploads inherit the same setting. aws-kms switches to SSE-KMS, using the AWS managed key unless kms_key_id names a customer managed key. MinIO honors SSE headers only when a KMS/KES is configured; for a MinIO deployment without one, set server_side_encryption = "none" explicitly or writes fail with an SSE error.

Config keyDefaultPurpose
storage.cold.backendnonenone, memory, or s3
storage.cold.rootnonePrefix prepended to every cold key. Use a date-stamped value for benchmark runs so cleanup can't touch production data
storage.cold.s3.bucketnoneRequired when backend = "s3"
storage.cold.s3.server_side_encryptionaes256SSE mode for every object write: aes256 (SSE-S3), aws-kms, or none
storage.cold.s3.kms_key_idnoneCustomer managed KMS key; only valid with aws-kms
storage.cold.flush_interval1sBackground flush worker tick interval
storage.cold.flush_size8MiBTarget bytes flushed per group per tick
storage.cold.flush_max_concurrency4Parallel cold writes in flight
storage.cold.max_hot_size_per_group64MiBBackpressure ceiling: when a group's hot bytes exceed this, new writes get HTTP 503 until the flush catches up. Explicit 0 disables the cap

Flush defaults are conservative, so tune them under load. A typical benchmark profile drops the interval to 200ms, raises concurrency to 32, and bumps the per-group ceiling. Advanced deployments can split the flush threshold and batch size with storage.cold.flush_min_hot_size and storage.cold.flush_max_size, but the single storage.cold.flush_size knob is the normal path. Once a group's accumulated hot payload reaches the threshold, Ursula packs eligible slices from that group into one immutable object up to the configured maximum; large single-stream slices keep the same direct-object layout.

Raft snapshotting does not force a partial cold flush. Payload below the cold threshold stays in the bounded hot tail and is included in the next group snapshot, so a short snapshot interval does not turn small tails into small cold objects.

Helm and the EC2 scripts map their deployment inputs onto these same keys, so this table applies to every deployment method.

Complete examples

Disk-backed single node (streams survive a restart):

[server]
listen = "127.0.0.1:4437"

[raft]
node_id = 1
group_count = 16

[raft.wal]
backend = "disk"
path = "./data"
ursula --config ./ursula.toml

Three-node durable cluster (same file on each host, override the node ID per host):

[server]
listen = "0.0.0.0:4437"

[runtime]
core_count = 16

[raft]
node_id = 1
group_count = 256
init_membership_per_group = true

[raft.wal]
backend = "disk"
path = "/var/lib/ursula"

[storage.cold]
backend = "s3"
root = "ursula-prod-20260518"

[storage.cold.s3]
bucket = "my-ursula-bucket"
region = "us-east-1"

[[raft.peers]]
node_id = 1
url = "http://10.0.0.1:4437"

[[raft.peers]]
node_id = 2
url = "http://10.0.0.2:4437"

[[raft.peers]]
node_id = 3
url = "http://10.0.0.3:4437"
ursula --config /etc/ursula/ursula.toml --node-id 1

See Deploy a Cluster for the full deployment walk-through.