Configuration
Ruvio resolves settings in this order:
- built-in defaults,
- a TOML file passed with
--config, RUVIO_*environment variables,- explicit command-line options.
All merged settings and cross-field relationships are validated before a listener is opened. Unknown TOML fields are rejected.
address = "127.0.0.1:6379"shards = 1port_mode = "per-shard" # shards > 1: "per-shard" (default) = port + i per shard and cluster discovery; "single" (deprecated) = one port only. Both forward a key's command to its owning shardadvertised_host = "127.0.0.1" # host in CLUSTER SLOTS/SHARDS/NODES# advertised_port = 6379 # shard 0's announced port; omitted = bind porthot_keys = [] # Redis globs; empty keeps every key on its shardshutdown_grace_ms = 30000metrics_address = "127.0.0.1:9121" # empty/omitted disables HTTP scrape
# key_stats = true # RUVIO HOTKEYS (1/1024 sampled, 128 entries) and RUVIO BIGKEYS (SET values >= 1 MiB, last 64)# slowlog_threshold_micros = 10000 # SLOWLOG: commands slower than this (negative = off, 0 = all); only the generic dispatcher is timed# slowlog_max_len = 128 # SLOWLOG entries kept per shard# latency_histograms = true # per-command latency histograms (INFO latencystats, INFO prometheus); false = off
[protocol]max_arguments = 1048576max_key_bytes = 1048576max_value_bytes = 16777216max_command_bytes = 67108864max_pipeline_commands = 1024max_connection_buffer_bytes = 67108864 # byte limits in [protocol] are capped at 2147483648 (2 GiB)databases = 16 # SELECT 0..databases-1; 1 to 256
[timeouts]idle_ms = 300000read_ms = 30000write_ms = 30000
[connections]max_global = 10000max_per_ip = 256 # lowered to max_global at startup when it is higher, with a warningmax_per_user = 0 # 0 disables the per-ACL-user connection cap; a higher value is lowered to max_globalmax_commands_per_second = 0 # 0 disables the per-connection command rate
[requests]max_queued_requests = 1024max_output_buffer_bytes = 67108864 # unsent reply bytes per connection; a pipeline sends replies as it goes
[persistence]profile = "memory" # memory, snapshot, everysec, or alwaysdata_dir = "./data"wal_segment_bytes = 268435456sync_interval_ms = 1000snapshot_max_bytes = 17179869184snapshot_journal_bytes = 67108864snapshot_interval_ms = 60000snapshot_min_mutations = 0cdc_retain_bytes = 0replica_retain_max_bytes = 1073741824
[replication]role = "off" # off, primary, or replicalisten = "127.0.0.1:26379" # primary base address; shard i uses port + iprimary = "127.0.0.1:26379" # replica dials this base address; a hostname is allowed
[security]requirepass = ""protected_mode = truetls_cert = "" # PEM certificate chain; empty disables TLStls_key = "" # PEM private key; must be set with tls_cert
[[security.users]]name = "app"rules = "on >app-secret ~app:* +@read +ping +auth +info"
[memory]max_shard_bytes = 0 # 0 disables the shard admission budgetmax_process_bytes = 0 # 0 disables the process budgetreserved_headroom_bytes = 16777216maxmemory_policy = "noeviction"maxmemory_samples = 16 # eviction candidates examined per decisionactive_expire_keys = 16 # expired keys reaped before each commandactive_expire_maintenance_keys = 16 # keys per expiry-timer batchhash_max_compact_entries = 64hash_max_compact_value_bytes = 64set_max_intset_entries = 512 # integer-only sets up to this size stay a sorted arraylist_max_compact_entries = 128 # lists up to this size stay in one packed bufferlist_max_compact_value_bytes = 64
[scripts]max_bytes = 16384max_cached = 1024max_calls = 1024max_instructions = 1000000 # each shard's Lua state is also capped at 128 MiB of memorymax_time_ms = 1000 # wall-clock limit per script, at most 3600000; see belowUse ruvio --help for CLI overrides; every environment variable is listed in
Environment variables. When --config is omitted, the process loads RUVIO_CONFIG, then
ruvio.toml beside the binary, then /etc/ruvio/ruvio.toml, then ./ruvio.toml.
A missing file is created from the built-in defaults. Environment variables and
CLI flags still win. The file is left as it is unless you pass --write-config,
which writes the effective values back into it at startup, so a process started
with --shards 3 never changes what the next process reads. The file is not
watched: an edit applies only on the next start.
Shard count, bind address, and durability cannot change under a live process.
Environment variables
Section titled “Environment variables”Each variable overrides one file setting, and a command-line flag, where there is one, overrides the variable. Booleans accept 1/0, true/false, yes/no and on/off; durations are milliseconds.
| Variable | Flag | File setting | Notes |
|---|---|---|---|
RUVIO_ADDR |
--address |
address |
|
RUVIO_SHARDS |
--shards |
shards |
|
RUVIO_PORT_MODE |
port_mode |
||
RUVIO_ADVERTISE_HOST |
--advertised-host |
advertised_host |
|
RUVIO_ADVERTISE_PORT |
--advertised-port |
advertised_port |
|
RUVIO_CPUSET |
--cpu-set |
cpu_set |
|
RUVIO_CORE |
cpu_set |
Older name; read only when RUVIO_CPUSET is unset |
|
RUVIO_SHUTDOWN_GRACE_MS |
--shutdown-grace-ms |
shutdown_grace_ms |
|
RUVIO_SLOWLOG_THRESHOLD_MICROS |
slowlog_threshold_micros |
||
RUVIO_SLOWLOG_MAX_LEN |
slowlog_max_len |
||
RUVIO_LATENCY_HISTOGRAMS |
latency_histograms |
||
RUVIO_KEY_STATS |
key_stats |
||
RUVIO_MAX_ARGUMENTS |
protocol.max_arguments |
||
RUVIO_MAX_KEY_BYTES |
protocol.max_key_bytes |
||
RUVIO_MAX_VALUE_BYTES |
protocol.max_value_bytes |
||
RUVIO_MAX_COMMAND_BYTES |
protocol.max_command_bytes |
||
RUVIO_MAX_PIPELINE_COMMANDS |
protocol.max_pipeline_commands |
||
RUVIO_MAX_CONNECTION_BUFFER_BYTES |
protocol.max_connection_buffer_bytes |
||
RUVIO_DATABASES |
protocol.databases |
||
RUVIO_IDLE_TIMEOUT_MS |
--idle-timeout-ms |
timeouts.idle_ms |
|
RUVIO_READ_TIMEOUT_MS |
--read-timeout-ms |
timeouts.read_ms |
|
RUVIO_WRITE_TIMEOUT_MS |
--write-timeout-ms |
timeouts.write_ms |
|
RUVIO_MAX_CONNECTIONS |
--max-connections |
connections.max_global |
|
RUVIO_MAX_CONNECTIONS_PER_IP |
--max-connections-per-ip |
connections.max_per_ip |
|
RUVIO_MAX_CONNECTIONS_PER_USER |
--max-connections-per-user |
connections.max_per_user |
|
RUVIO_MAX_COMMANDS_PER_SECOND |
--max-commands-per-second |
connections.max_commands_per_second |
|
RUVIO_MAX_QUEUED_REQUESTS |
--max-queued-requests |
requests.max_queued_requests |
|
RUVIO_MAX_OUTPUT_BUFFER_BYTES |
--max-output-buffer-bytes |
requests.max_output_buffer_bytes |
|
RUVIO_DURABILITY |
--durability |
persistence.profile |
memory, snapshot, everysec or always |
RUVIO_DATA_DIR |
--data-dir |
persistence.data_dir |
|
RUVIO_WAL_SEGMENT_BYTES |
--wal-segment-bytes |
persistence.wal_segment_bytes |
|
RUVIO_WAL_SYNC_INTERVAL_MS |
--wal-sync-interval-ms |
persistence.sync_interval_ms |
|
RUVIO_EXPERIMENTAL_SYNC_WORKER |
persistence.sync_worker |
Older boolean form (true = on); RUVIO_SYNC_WORKER wins |
|
RUVIO_SYNC_WORKER |
persistence.sync_worker |
||
RUVIO_WAL_PREFILL |
auto (default), on or off. Linux only: zero-fill WAL segments ahead of the cursor on a helper thread. auto does it for always only; under everysec the zero-fill doubled the bytes a throughput-capped disk saw and halved SET throughput. CONFIG GET wal-prefill shows the mode |
||
RUVIO_SNAPSHOT_MAX_BYTES |
--snapshot-max-bytes |
persistence.snapshot_max_bytes |
|
RUVIO_SNAPSHOT_JOURNAL_BYTES |
--snapshot-journal-bytes |
persistence.snapshot_journal_bytes |
|
RUVIO_SNAPSHOT_INTERVAL_MS |
--snapshot-interval-ms |
persistence.snapshot_interval_ms |
|
RUVIO_SNAPSHOT_MIN_MUTATIONS |
--snapshot-min-mutations |
persistence.snapshot_min_mutations |
|
RUVIO_CDC_RETAIN_BYTES |
persistence.cdc_retain_bytes |
||
RUVIO_REPLICA_RETAIN_MAX_BYTES |
persistence.replica_retain_max_bytes |
||
RUVIO_REQUIREPASS |
--requirepass |
security.requirepass |
|
RUVIO_TLS_CERT |
--tls-cert |
security.tls_cert |
|
RUVIO_TLS_KEY |
--tls-key |
security.tls_key |
|
RUVIO_TLS_CLIENT_CA |
--tls-client-ca |
security.tls_client_ca |
|
RUVIO_TLS_CLIENT_AUTH |
--tls-client-auth |
security.tls_client_auth |
|
RUVIO_ACL_FILE |
security.acl_file |
||
RUVIO_PROTECTED_MODE |
--protected-mode |
security.protected_mode |
|
RUVIO_ALLOW_UNAUTHENTICATED |
security.allow_unauthenticated_public |
||
RUVIO_METRICS_TOKEN |
security.metrics_token |
||
RUVIO_REPLICATION_PASSWORD |
security.replication_password |
||
RUVIO_MAXMEMORY |
--maxmemory |
memory.max_process_bytes |
|
RUVIO_MAX_SHARD_BYTES |
--max-shard-bytes |
memory.max_shard_bytes |
|
RUVIO_MEMORY_HEADROOM_BYTES |
--memory-headroom-bytes |
memory.reserved_headroom_bytes |
|
RUVIO_MAXMEMORY_POLICY |
--maxmemory-policy |
memory.maxmemory_policy |
|
RUVIO_MAXMEMORY_SAMPLES |
--maxmemory-samples |
memory.maxmemory_samples |
|
RUVIO_ACTIVE_EXPIRE_KEYS |
--active-expire-keys |
memory.active_expire_keys |
|
RUVIO_ACTIVE_EXPIRE_MAINTENANCE_KEYS |
--active-expire-maintenance-keys |
memory.active_expire_maintenance_keys |
|
RUVIO_HASH_MAX_COMPACT_ENTRIES |
--hash-max-compact-entries |
memory.hash_max_compact_entries |
|
RUVIO_HASH_MAX_COMPACT_VALUE_BYTES |
--hash-max-compact-value-bytes |
memory.hash_max_compact_value_bytes |
|
RUVIO_SET_MAX_INTSET_ENTRIES |
--set-max-intset-entries |
memory.set_max_intset_entries |
|
RUVIO_LIST_MAX_COMPACT_ENTRIES |
--list-max-compact-entries |
memory.list_max_compact_entries |
|
RUVIO_LIST_MAX_COMPACT_VALUE_BYTES |
--list-max-compact-value-bytes |
memory.list_max_compact_value_bytes |
|
RUVIO_NOTIFY_KEYSPACE_EVENTS |
notify_keyspace_events |
||
RUVIO_METRICS_ADDR |
--metrics-address |
metrics_address |
|
RUVIO_SCRIPT_MAX_BYTES |
scripts.max_bytes |
||
RUVIO_SCRIPT_MAX_CACHED |
scripts.max_cached |
||
RUVIO_SCRIPT_MAX_CALLS |
scripts.max_calls |
||
RUVIO_SCRIPT_MAX_INSTRUCTIONS |
scripts.max_instructions |
||
RUVIO_SCRIPT_MAX_TIME_MS |
scripts.max_time_ms |
||
RUVIO_HOT_KEYS |
hot_keys |
Comma-separated globs | |
RUVIO_REPLICATION_ROLE |
replication.role |
||
RUVIO_REPLICATION_LISTEN |
replication.listen |
||
RUVIO_REPLICATION_PRIMARY |
replication.primary |
Flags with no variable:
| Flag | What it does |
|---|---|
--config |
Config file to read; see the lookup order above |
--force-promote-incomplete |
Serve a recoverable but incomplete replica as a writer, accepting possible data loss |
--write-config |
Write the effective settings back into the config file at startup |
--licenses |
Print Ruvio’s license and the third-party notices, then exit |
The process is evented. There is no runtime switch in the file, on the command line, or in the environment. INFO still reports ruvio_backend:evented.
Default features are mimalloc-allocator. --no-default-features keeps the same runtime and uses the system allocator. One shard or consecutive ports, and TLS, are available on Linux, macOS, and Windows.
Invalid limits, zero timeouts, and malformed CPU sets fail startup.
max_per_user / RUVIO_MAX_CONNECTIONS_PER_USER counts authenticated ACL
identities and may be 0. max_commands_per_second /
RUVIO_MAX_COMMANDS_PER_SECOND is per connection and may be 0. A per-user
cap cannot exceed max_global.
WAL-backed profiles exclusively lock <data_dir>/LOCK before opening a
listener. everysec acknowledges after the bytes are written to the kernel and
runs fdatasync in the background every sync_interval_ms (default 1000).
That interval is the loss window: 60000 is about one minute, 500 is twice
a second. There is no separate everyminute profile; the contract stays
everysec and the number is the user’s. Stalls can make the window longer.
always ignores sync_interval_ms and fdatasyncs a client batch before the
replies are sent. A persistence write or sync failure puts that shard into a
visible error state and further mutations return MISCONF. After a failed
write (for example a full disk) the shard resumes by itself once the disk
accepts writes again. After a failed fdatasync it does not: the kernel may
have dropped the unsynced pages and report a later sync as clean, so mutations
stay refused (reads keep working) until a restart, whose recovery replays only
what actually reached the disk.
One WAL record must fit in a segment: a write, a MULTI/EXEC batch, or a
script’s writes whose record is larger than wal_segment_bytes is refused with
ERR write batch exceeds wal_segment_bytes. Nothing of it is applied or logged,
the shard stays healthy (this is not a MISCONF state), and the refusal is
logged at most once a minute. Raise wal_segment_bytes or split the write.
Sequence-barrier sync worker
Section titled “Sequence-barrier sync worker”persistence.sync_worker = "auto" | "on" | "off" (env RUVIO_SYNC_WORKER) moves
the grouped fdatasync of always durability off the shard thread.
The default auto turns it on only for Linux, always durability, one shard and no
hot_keys; everywhere else it stays off. on forces it
(other profiles and hot_keys fail validation) and off
returns to the shard-thread sync. The old experimental_sync_worker /
RUVIO_EXPERIMENTAL_SYNC_WORKER keys still work and map to on / off.
CONFIG GET sync-worker shows the mode and CONFIG GET sync-worker-active
the resolved value.
Each shard has one long-lived sync worker, one active sync and one coalescing
pending slot. The shard retains WAL encoding, append and mutation ownership;
the worker uses a segment-scoped cloned file handle without taking the shard’s
persistence mutex during sync_data. Grouped response batches capture a
sequence and asynchronously wait for the durable cursor before flushing.
Read-only batches and outbound notification frames also wait for already
observed writes. A conservative 64 KiB unsynchronized-byte threshold drains the
group between commands; one command’s WAL records may exceed that threshold,
bounded by existing protocol/segment limits. Existing connection and output
limits still bound pending replies. No per-command thread/task/timer or queue
entry is introduced.
This is sync offload, not the complete persistence-worker architecture.
Append I/O, fallback mutations, segment rotation and checkpoint operations
remain synchronous. Rotation/drain and shutdown can still block on the OS’s
disk call; a client durability wait uses the write timeout but does not cancel
an in-flight fsync or replay a write. Worker errors are latched, logged and fail
barriers without emitting buffered success responses; after a persistence
failure (disk full, I/O error) batches that wrote nothing, such as reads and
PING, are still answered and writes return MISCONF. Worker sync metrics
count worker calls, not inline fallback/rotation syncs.
Measured on Linux (4-core VM, Premium SSD): always SET throughput about 2x
after the WAL began filling its preallocated chunks with zeros on a helper
thread. More
shards do not speed up always: disk flush latency, not shard CPU, is the limit.
SAVE and BGSAVE publish a durable checkpoint on the shard worker, persist
the last-seen clock floor to <data_dir>/CLOCK, and then delete WAL segments
covered by that checkpoint. SAVE replies once the checkpoint is published.
BGSAVE does not fork; it starts the same in-process checkpoint and replies
Background saving started at once, before the checkpoint is written. The
shard writes it in slices between requests, and INFO reports
rdb_bgsave_in_progress:1 until it is published; a failure after the reply
is only logged. With several shards SAVE asks every shard for a checkpoint
taken after the request and replies once a round holding all of them is
published; if any shard’s checkpoint or the round fails, it replies
MISCONF persistence error at once, as with one shard. Set
snapshot_interval_ms to schedule checkpoints after that interval when at
least snapshot_min_mutations mutating commands have run (0 means any
mutation). snapshot_interval_ms = 0 disables the
scheduler. SAVE still works. Recovery uses
max(CLOCK, checkpoint.created_at) as a monotonic clock floor so TTLs do not
appear expired if the wall clock moves backwards.
A background checkpoint walks the keyspace in short slices between requests and
keeps the old copy of each key written meanwhile in a journal bounded by
snapshot_journal_bytes. When writes overflow it, the attempt restarts with
longer slices, then with a larger journal (at most a quarter of the shard’s
data), then with slices of up to 256 ms that wait for the snapshot writer. If
even those cannot outpace the writes, the attempt is given up, logged, and
retried after 5 seconds; INFO counts these in snapshot_overload_skips_total
(snapshot_aborts_total counts every restart). The WAL keeps every write
meanwhile. Each retry starts with the longest slices and raises the journal’s
cap to half, then all of the shard’s data; with max_shard_bytes set, these
caps also stay within the memory left under that limit. If even the largest
journal overflows, the next retry falls back to long blocking slices (2 s,
doubling after each abort) and logs an error: memory stays bounded, and this
shard’s clients stall during those slices until the walk completes. On a large
shard one stall can be long (a slice may reach about twice a full walk), but it
ends. The first journal level (a quarter of the data) is not limited by the
memory left under max_shard_bytes. A completed or failed checkpoint resets
the ladder. SAVE walks in one go and is not affected.
Change stream
Section titled “Change stream”CHANGES SUBSCRIBE [FROM <shard>:<seq>|NOW] [MATCH glob] [TYPES a,b] [VALUES] turns this
shard’s WAL into a push stream (RESP3 >; RESP2 clients get plain arrays). The reply is the
cursor the stream starts after. Each event is
["change", cursor, db, type, key|nil, value|nil]; type is one of put, json, del,
expire, rename, list, set, hash, zset, stream, flush, other. value is filled
only with VALUES, and only for string and JSON writes. A record with several keys produces one
event per key, all with the same cursor, written together. A client stores the cursor of the last
event it processed and passes it as FROM after a disconnect or restart: no event is lost or
repeated. Filtered-out records still move the cursor (["cursor", c] about once a second).
Only durable records are streamed. A shard is its own sequence, so a cluster client subscribes on
each shard it needs; the cursor names its shard and a wrong one is refused.
cdc_retain_bytes (environment RUVIO_CDC_RETAIN_BYTES, default 0) is how much WAL a
checkpoint keeps for subscribers, rounded to whole segments, whether or not anyone is
connected. 0 keeps nothing extra and changes nothing. A cursor older than the window gets
["resync", reason] and the connection closes: reload fully, then FROM NOW. A subscriber too
slow to drain its queue is disconnected and resumes from its cursor. A stream needs a durability
profile with a WAL, and is refused on a replica. Expiry that happens lazily on read is not in the
WAL, so it produces no event; the expire event carries the deadline change, not the removal.
replica_retain_max_bytes (environment RUVIO_REPLICA_RETAIN_MAX_BYTES, default 1 GiB, 0 = no
limit) caps the WAL a connected replica that has not yet received it may keep on the primary. Past
the cap the primary drops that hold and compacts as usual; the replica finds its cursor gone and
takes a fresh snapshot (the fail-closed catch-up path), it is never promoted with a gap.
INFO shows replica_retain_dropped, how many times a hold was dropped.
ruvio-ctl check and inspect validate the same files without taking LOCK
or truncating a torn WAL tail. ruvio-ctl backup exclusively locks the source
and copies CURRENT, CLOCK, checkpoints/, wal/, and any per-shard
REPLICA_CATCHUP_#### markers. ruvio-ctl restore refuses a destination that
already has those files or markers unless --force is set. Integrity checks
do not certify replica catch-up completeness. See Recovery, Backup, and Integrity.
INFO prometheus returns Prometheus text for the receiving shard. When
metrics_address is set, that shard also serves GET /metrics on a separate
listener. Multi-shard layouts use consecutive metrics ports starting at the
configured address. The metrics range must not overlap the RESP port range.
hot_keys / RUVIO_HOT_KEYS is a list of Redis-style * / ? globs
(hot_keys = ["lock:*", "session:*"] or RUVIO_HOT_KEYS=lock:*,session:*).
The default is empty: the feature is off and the shard GET path takes no extra
lock. A matching key is served from a process-global string store before hash
slot / MOVED, so two shards in one process see the same lock:foo. With
one shard every key is already on it, so hot_keys is ignored there (with a
warning). Allowed commands are GET, SET (including NX / XX / EX /
PX / EXAT / PXAT / KEEPTTL), and DEL.
Any other command on a matching key returns
-ERR hot key does not support <cmd> and does not write the shard. Inside
MULTI a command on a matching key returns
-ERR hot keys cannot be used inside MULTI and EXEC then aborts with
EXECABORT: the hot store cannot join the transaction. Empty glob
entries fail startup. Enable the prefix on empty data; existing shard keys are
not migrated. Checkpoints wait for a virtual hot partition (shards + 1)
before publishing CURRENT. SAVE on a primary with hot_keys writes the hot
partition into the same checkpoint. A replica behind a deleted WAL prefix
receives that checkpoint.
replication.role / RUVIO_REPLICATION_ROLE is off (default), primary, or
replica. replication.listen / RUVIO_REPLICATION_LISTEN is the primary’s
base address. replication.primary / RUVIO_REPLICATION_PRIMARY is the
address a replica dials. replication.listen must be a numeric socket address.
replication.primary may be a numeric address or host:port, resolved again on
each dial.
Shard i uses that base port plus i. Role off opens no replication port
and dials nobody. A primary binds replication.listen (loopback in the sample
above). A replica dials replication.primary and does not listen. A primary keeps
each replica on its own stream, up to 16.
INFO reports replication_role, wal_last_sequence, and
wal_durable_sequence on every shard. A primary also reports
replication_replicas, replication_replica_sequence, and
replication_send_lag. A replica reports master_link_status (up or
down), master_last_io_seconds_ago (-1 before the first frame),
slave_repl_offset and master_sync_in_progress. When a checkpoint must be installed, the replica
persists a REPLICA_CATCHUP_#### marker under data_dir before replacing its
local snapshot/WAL base. Until it has replayed and flushed WAL through the
primary’s catch-up watermark, client reads return LOADING; after 30 seconds
without completion they return MASTERDOWN. The marker survives restart and
backup/restore. A replica with an incomplete marker requests a fresh snapshot
when the primary becomes available; do not delete the marker by hand. No
additional TOML setting is needed for this protection.
Clocks. Every node expires keys by its own clock, and the primary does not replicate expiry deletes. Keep primary and replicas in sync with NTP. A replica whose clock runs ahead can drop a key early; if the primary then extends that key’s TTL, the replica no longer has it and requests a full copy of the shard. A TTL change whose new deadline has already passed is applied without a resync.
Changing replication.role from replica to off or primary is a manual
promotion, not an automatic failover. If any shard still has a catch-up
marker, normal writer startup refuses it. ruvio --config ./ruvio.toml --force-promote-incomplete allows a recoverable but possibly incomplete
replica to start as a writer only for that invocation; it is a CLI flag,
not a TOML or environment setting, and is never written back to the config.
It cannot be used while role = "replica" and does not bypass corrupt
checkpoint/WAL validation or recover missing writes. The marker stays on disk,
so every subsequent writer restart requires another explicit force decision.
Fence the old primary to prevent split brain. Follow the per-shard sequence
checks and procedure in Recovery, Backup, and Integrity.
A later handshake does not close an earlier replica. At the limit the primary closes the new connection. Primary and replicas must run the same Ruvio version: a primary refuses a replica of another version and both sides log a warning (see Recovery, Backup, and Integrity). Startup refuses replication unless the process can copy a WAL:
memoryandsnapshotreturnreplication requires a WAL-backed durability profile.- Role
primarywithoutreplication.listenreturnsreplication primary requires replication.listen. - Role
replicawithoutreplication.primaryreturnsreplication replica requires replication.primary. - A non-numeric
replication.listenreturnsreplication.listen must be a numeric socket address.replication.primaryaccepts a numeric address or a resolvablehost:port; an invalid address is rejected. - Role must be
off,primary, orreplica, or startup returnsreplication role must be off, primary, or replica. - The replication port range must not overlap RESP (
replication port range overlaps the RESP listener range) or metrics (replication port range overlaps the metrics listener range).
Script limits are RUVIO_SCRIPT_MAX_BYTES, RUVIO_SCRIPT_MAX_CACHED,
RUVIO_SCRIPT_MAX_CALLS, RUVIO_SCRIPT_MAX_INSTRUCTIONS, and
RUVIO_SCRIPT_MAX_TIME_MS. Zero is rejected, and max_time_ms above
3,600,000 returns script time limit must be at most 3600000 ms. The time
limit is checked every 1,000 Lua instructions, more often once the shard’s Lua
heap passes 1 MiB (down to every 10 at 100 MiB), before each library call that
is charged by the size of its data, and before each ruvio.call. A single
operation is not interrupted, so a script can run past the limit by the time
of the last few operations on its largest values.
The default bind address is loopback. A non-loopback bind without
requirepass keeps protected_mode on by default and rejects non-loopback
clients. Turning protected mode off on a public bind without a password
refuses to start. Container images set RUVIO_PROTECTED_MODE=false because
published ports are not loopback inside the namespace, so they need
RUVIO_REQUIREPASS or an ACL password. RUVIO_ALLOW_UNAUTHENTICATED=true
is the explicit exception for a network that is already private.
RUVIO_METRICS_TOKEN is required when metrics_address is not loopback;
scrape requests then send Authorization: Bearer. RUVIO_REPLICATION_PASSWORD
is the replication handshake secret (Garnet’s cluster password). When it is
empty the handshake uses requirepass. A public replication listen needs one
of those secrets. See Threat Model. AUTH uses
constant-time comparison and never logs the secret. requirepass,
metrics-token, and replication-password are redacted in CONFIG GET.
CONFIG GET * lists the effective settings for the process; CONFIG GET max-*
uses the same glob rules as SCAN. Redis compatibility keys save (empty)
and appendonly (no) remain present. CONFIG SET is not supported.
TLS is supported on portable and evented. Set tls_cert / tls_key, RUVIO_TLS_CERT /
RUVIO_TLS_KEY, or --tls-cert / --tls-key to PEM files. Both paths must
be set together. The accept path completes the rustls handshake before
reading RESP and reloads material when either file’s mtime changes.
io-uring plus TLS fails startup; terminate TLS in front of that adapter.
When TLS is enabled, plaintext clients are closed at handshake. Redis clients
must enable TLS (rediss://, --tls, or the library equivalent) and trust
the configured certificate. tls_client_ca / RUVIO_TLS_CLIENT_CA /
--tls-client-ca is a PEM issuer. A presented client certificate must chain
to it. tls_client_auth / RUVIO_TLS_CLIENT_AUTH / --tls-client-auth
rejects a handshake that does not present one. Neither setting is consulted
on a plaintext connection.
ACL users are configured in [[security.users]] with Redis-style rule tokens:
on/off, >password, nopass, ~pattern, +@category, -@category,
+command, and -command. The implicit default user is on nopass ~* +@all
until requirepass or a default user entry sets a password. AUTH password
targets default; AUTH username password selects a named user. The operator
UI sign-in form is this same AUTH. An empty user and password send nothing
and the session stays default. Command and
key checks run on the connection after AUTH using a copied rule set; they do
not take a shard lock. ACL SETUSER uses the same tokens and leaves an
open connection on the rules copied at its last AUTH. security.acl_file
or RUVIO_ACL_FILE is the ACL file. When that file already exists at
startup it is the user list. ACL SAVE writes the live directory there,
as password hashes (#<hex>; a plain >password line in a file you write
yourself is hashed when it is loaded). ACL LOAD reads it back and replaces the directory.
GET and SET do not open the file. ACL LIST still redacts passwords.
An empty path refuses ACL SAVE and ACL LOAD. Restart without a file
reads [[security.users]] again.
ACL LIST redacts passwords. INCR/DECR/INCRBY/DECRBY share the incr
ACL name.
Shard memory admission defaults to noeviction. index_memory_bytes and the
admission charge include retained key and TTL deadline-index capacities; TTL
keys can therefore reach a configured budget sooner than keys without TTL.
This modeled budget is not an exact process RSS cap (allocator, runtime, and
persistence allocations also contribute). When max_shard_bytes is set,
writes that would exceed the budget minus reserved_headroom_bytes return
Redis-style OOM unless maxmemory-policy / RUVIO_MAXMEMORY_POLICY /
--maxmemory-policy is allkeys-lru, allkeys-lfu, volatile-lru, or
volatile-ttl. Commands that only remove or move data (DEL, HDEL,
LPOP, …) are not refused; see Memory Accounting,
including why memory can stay at the limit until a running checkpoint ends.
Eviction is shard-local. Each decision compares up to
maxmemory_samples eligible live keys (default 16, range 1–64); the engine deletes
at most 16 keys per command. Sampling rotates over index slots and permitted
logical databases instead of repeatedly starting at the first keys. The production
hash index resumes at bucket positions. Expired and active-command protected keys
are skipped. Volatile policies sample only keys with a TTL from the deadline
lookup and return OOM immediately when the shard has no TTL keys. Under
allkeys policies, finding eligible candidates can still require a full pass when
most keys are expired or protected: maxmemory_samples is not a scanned-key or
latency limit. This preserves admission when an eligible key exists outside the
initial sample. Under memory pressure with an eviction policy enabled, admission
first tries one additional, bounded 16-entry expiry sweep before sampling
victims, even if the ordinary command sweep has already run. This can recover
space without eviction but does not guarantee a bound on the subsequent scan.
active_expire_keys (default 16, range 1–1024) bounds each ordinary command
sweep (and the separate hash-field maintenance pass). The independent
active_expire_maintenance_keys (default 16, range 1–1024) sets the batch size
for background key-expiry maintenance; changing it does not enlarge the command
sweep. Deadline lookup prioritizes the earliest expiry in each database, even
if many later deadlines were registered first; databases rotate when a sweep
fills its budget. Key-expiry maintenance runs approximately every 25 ms,
separately from the 100 ms general maintenance tick. It repeats full batches
while due keys remain, stopping after 750 µs of elapsed cleanup or 1,024 keys
per expiry tick. The time limit is checked between batches, not during a key
removal, so larger maintenance batches can lengthen a single event-loop stall;
it is not a strict shard-latency limit. If more keys expire than the combined
command, timer and pressure sweeps can remove, physical memory can remain
occupied until later turns; reads still treat expired keys as absent. Increasing
the command budget raises synchronous work on every command; increasing only
the maintenance batch may change idle drain at the cost of longer
non-preemptible maintenance turns. In the measured 50k SET/s WAL burst,
batches 32 and 64 did not consistently outperform the default 16; tune only
against representative backlog and shard-latency measurements.
A write larger than the shard budget is rejected without evicting. Connection,
output, persistence, and snapshot-journal charges are included in charged_bytes.
Hashes stay in a compact field list until hash_max_compact_entries or a
field/value longer than hash_max_compact_value_bytes; past that, field and
value share one arena block. A set of canonical integers stays a sorted integer
array up to set_max_intset_entries (0 turns it off); other sets of more than
128 members keep their bytes in an arena. A list stays in one packed buffer up
to list_max_compact_entries items of at most list_max_compact_value_bytes
each, then becomes a chain of 8 KiB packed nodes. Sorted sets have one
encoding. The
key index, string buffers, list rings, and set, hash, and sorted-set tables
shrink after deletes or a much smaller overwrite once they are under a quarter
full. The hot-key map uses the same rule. Environment overrides are
RUVIO_MAXMEMORY_SAMPLES, RUVIO_ACTIVE_EXPIRE_KEYS,
RUVIO_ACTIVE_EXPIRE_MAINTENANCE_KEYS, RUVIO_HASH_MAX_COMPACT_ENTRIES,
RUVIO_HASH_MAX_COMPACT_VALUE_BYTES, RUVIO_SET_MAX_INTSET_ENTRIES,
RUVIO_LIST_MAX_COMPACT_ENTRIES, and RUVIO_LIST_MAX_COMPACT_VALUE_BYTES.
CLI flags use the same names with dashes.
notify_keyspace_events (environment RUVIO_NOTIFY_KEYSPACE_EVENTS) is
empty by default. K publishes __keyspace@<db>__:<key> and E publishes
__keyevent@<db>__:<event>, where <db> is the key’s logical database.
Classes are g (delete), $ (SET), x (expired), e (evicted), and A
for those four. Without K or E the feature stays off. A client subscribes
with SSUBSCRIBE on the exact channel. Delivery is asynchronous smessage
and is not written to the WAL.
To receive both expiration and eviction notices, enable keyevent delivery
(E) and both classes (x and e) in the TOML file:
notify_keyspace_events = "Exe"The setting is read at startup; restart Ruvio after changing it. Subscribe to
the exact channel for the key’s database, for example
SSUBSCRIBE __keyevent@0__:expired or
SSUBSCRIBE __keyevent@0__:evicted. PSUBSCRIBE patterns do not receive
keyspace notices. On these keyevent channels the message payload is the key
name, not its value. Notifications are live, best-effort messages: they are
not replayed to later subscribers and are not durable.
The C# client provides convenience methods that form these channel names from the selected database:
await db.SubscribeExpiredAsync(message => Console.WriteLine($"Expired key: {message.Payload}"));await db.SubscribeEvictedAsync(message => Console.WriteLine($"Evicted key: {message.Payload}"));Eviction notices occur only when memory pressure actually triggers an eviction.
noeviction is the default; configure a shard memory budget and an eviction
policy if eviction is desired. In a multi-shard deployment, notifications are
local to the shard that owns the key. A subscription on one shard does not
collect events from every shard. The Operator Pub/Sub screen has matching
expired/evicted listeners; choose the database and shard to monitor there.
shards (environment RUVIO_SHARDS, flag --shards) starts that many
shard owners in one process. With the default port_mode = "per-shard" shard
i listens on the bind port plus i and owns slots i * 16384 / shards up to
the next shard’s first slot. A command for a key another shard owns is forwarded
to that shard and answered on the connection it came in on; the server never
sends MOVED (sharded Pub/Sub channels are the exception: SSUBSCRIBE on the
wrong shard is redirected, as a subscription cannot be forwarded). Cluster-aware
clients find the other shards from CLUSTER SLOTS, CLUSTER SHARDS, or
CLUSTER NODES, connect to each owner and pay no hop; any other client works on
any one port, with the hop. port_mode = "single" (deprecated: it logs a
warning at startup and will be removed; any one per-shard port already serves
every key) opens one port only, spreads connections over the shards, runs
keyless commands (DBSIZE, KEYS, FLUSHALL, SCAN) across every shard and
lets WAIT cover every shard; in per-shard those stay per shard, as in Redis
Cluster, and cluster clients fan them out themselves. Classic PUBLISH reaches subscribers on every shard in both modes.
advertised_host (RUVIO_ADVERTISE_HOST, --advertised-host, default
127.0.0.1) is the host the cluster replies name. advertised_port (RUVIO_ADVERTISE_PORT, --advertised-port) is the
port they name for shard 0; shard i is that port plus i. Leave it unset
when clients reach the bind ports directly. Set both when a container or load
balancer publishes the shards elsewhere, for example -p 7000-7003:6379-6382
with RUVIO_ADVERTISE_HOST=<host clients use> and RUVIO_ADVERTISE_PORT=7000.
cpu_set (RUVIO_CPUSET, --cpu-set) pins shard threads on Linux. It takes a
CPU list such as 2, 0-3 or 0,2,4-5; shard i runs on the ith listed
core and the list wraps when there are more shards than cores. Unset means no
pinning. A bad list or a core the process may not use only logs a warning. Other
platforms log that pinning is unsupported.
The shard count is read at start; changing it needs a restart on an empty data
directory.
[protocol] databases (environment RUVIO_DATABASES) is the number of
logical databases, 16 by default and 1 to 256. SELECT accepts 0 to
databases - 1. With shards > 1 only database 0 is allowed. CONFIG GET databases reports the value. See docs/LOGICAL_DATABASES.md.