Configuration
Configure Flo via flo.toml and CLI flags.
Flo is configured via a flo.toml file and CLI flags. CLI flags take the highest precedence: CLI flags > flo.toml > built-in defaults.
Config File
By default, Flo looks for flo.toml in the current directory. Override with --config:
flo server start --config /etc/flo/flo.toml
Generate a default config file with all options:
flo config init
Full Reference
[server]
[server]
port = 9000 # Client API port (binary protocol + WebSocket)
bind = "0.0.0.0" # Bind address
data_dir = "~/.flo/data" # Data directory for UAL segments and storage files
shards = 0 # Number of shards (0 = auto-detect CPU count)
# partition_count = 0 # Virtual partitions (0 = auto: max(4096, shards × 32))
shards and partition_count define the on-disk data layout. Cannot be changed after data exists without rebalancing.
[storage]
[storage]
durability = "async_flush" # sync | async_flush | ephemeral
hot_buffer_capacity = 67108864 # Per-partition ring buffer size in bytes (64 MB)
# hot_flush_seconds = 300 # Max seconds before hot → warm flush (0 = disabled)
# max_hot_entries = 0 # Max entries in hot tier before eviction (0 = capacity only)
# max_local_segments = 100 # Max warm segments before archival to cold tier
# enable_wal_truncation = true # Truncate WAL after safe segment flush
Durability modes:
| Mode | Behaviour | At risk on abrupt termination |
|---|---|---|
sync | Segment written and fsynced before the write is acknowledged | Nothing |
async_flush | Segments written and fsynced by a background task, at most once per second — the default | Up to ~1 second of acknowledged writes |
ephemeral | Never written to disk | Everything |
What async_flush risks, concretely. An acknowledged write lives only in
memory until the next segment flush, which runs at most once per second. If the
process is killed (SIGKILL, OOM kill, power loss, host failure) you can lose
up to roughly the last second of acknowledged writes. Everything older is on
disk, fsynced and published by atomic rename, so the store is never left
torn or half-written — the loss is a clean tail truncation, not corruption.
A graceful shutdown (SIGTERM, docker stop, flo server stop) flushes
before exiting, so a normal restart loses nothing. The one-second window only
applies when the process dies without running its shutdown path.
:::note
This window is not governed by hot_flush_seconds. That setting controls
hot → warm tier migration, which is a storage-layout concern and does not
affect what survives a crash.
:::
[logging]
[logging]
level = "info" # debug | info | warn | error
[auth]
[auth]
enabled = false
# jwt_secret = "your-256-bit-secret-key-here"
# jwks_url = "https://your-project.supabase.co/.well-known/jwks.json"
When enabled = true, all client connections require a valid JWT. Supports HS256 (shared secret) and RS256/ES256 (JWKS URL with key rotation).
[websocket]
[websocket]
rate_limit_requests = 1000 # Max requests per window (0 = unlimited)
rate_limit_window_ms = 1000 # Window size in milliseconds
ping_interval_ms = 30000 # Heartbeat ping interval (0 = disabled)
pong_timeout_ms = 10000 # Close connection if no pong within this time
[server] bind is the interface this node serves on — for clients and, in a
cluster, for peers. 0.0.0.0 (the default) listens everywhere and lets peers
reach the node at whatever address they see it from; a specific address is
also what the node advertises to its peers. --bind overrides it on the
command line.
[metrics]
[metrics]
enabled = true
# port = 0 # 0 = auto (listen_port + 1), so 9001 by default
# bind = "127.0.0.1" # Localhost only by default
Binds only when enabled = true. For a Prometheus server scraping from outside
the container, set bind = "0.0.0.0" — the default is loopback-only.
[dashboard]
[dashboard]
enabled = true
# port = 0 # 0 = auto (listen_port + 2), so 9002 by default
# bind = "127.0.0.1" # Localhost only by default
# cors_origins = "http://localhost:5173"
:::note
bind defaults to 127.0.0.1. In an orchestrated deployment a readiness probe
originating outside the container cannot reach a loopback listener, so the
default presents as a service that never becomes ready. Set bind = "0.0.0.0"
where the port is not publicly routable. See
Docker → Health Check.
:::
[cluster]
[cluster]
enabled = false # true starts the first member of a cluster (same as --cluster)
# secret = "..." # required whenever the peer port is bound; same value on every node
# node_id = 1
# raft_port = 0 # 0 = auto (listen_port + 500)
# seeds = ["192.168.1.10:9500"] # peer ports of members to join (same as --join)
# failover_timeout_ms = 1500 # a leader unheard for this long is replaced; minimum 100
secret is what a node proves it holds before another node treats it as a
peer: the Raft port moves terms, membership and log contents, so it is never
open. Every node in a cluster carries the same value; a node with a different
one is refused at the handshake and both sides log it. The secret itself never
crosses the wire. Generate one with openssl rand -base64 32. It can also come
from the FLO_CLUSTER_SECRET environment variable, for containers without a
config file. Starting the Raft listener without it is a startup error that
names the key.
node_id is read on first boot and then stored in the data dir, which wins on
every later boot; to change a node's id, start it from an empty data dir.
The peer port is bound only when this node can have peers: enabled = true
starts the first member of a cluster, seeds joins members that exist, and
the two are not combined. A plain single-node server does not listen on
listen_port + 500, so there is no undeclared port to account for; a
raft_port set without either role is refused at start. The startup banner
reports which applies.
failover_timeout_ms is the one timing setting: a leader unheard for between
half and all of it is replaced, heartbeats go out at a sixth of it. These are
the only keys [cluster] reads; any other key is refused at start, not
silently ignored. See Clustering.
[cold_storage]
[cold_storage]
# provider = "none" # none | file | s3
# upload_workers = 2
# restore_workers = 4
# [cold_storage.file]
# base_path = "/var/lib/flo/archive"
# sync_on_write = true
# [cold_storage.s3]
# bucket = "my-flo-cold-storage"
# region = "us-east-1"
# endpoint = "" # For S3-compatible services (MinIO, R2)
# use_path_style = false # Set true for MinIO
# use_tls = true
CLI Overrides
Common options can be set directly on the command line. These override everything:
flo server start \
--port 4444 \
--data-dir ./my-data \
--shards 4 \
--partitions 128 \
--log-level debug \
--log-format json \
--metrics-port 9101 \
--dashboard-port 9102 \
--no-metrics \
--no-dashboard
Clustering flags — the first member, then one that joins it:
flo server start --cluster
flo server start --join 192.168.1.10:9500