Running the server¶
rostam-server exposes the engine over three transports — REST, gRPC, and a
compact binary TCP protocol — all dispatching into the same store, so semantics
are identical regardless of how you connect.
# install the release binary (verifies the checksum), then run it
curl -fsSL https://rostamlabs.com/install.sh | sh
~/.local/bin/rostam-server -http 127.0.0.1:8080 -grpc 127.0.0.1:9090 -tcp 127.0.0.1:7000 -data ./data
# or run the container image (published on loopback only)
docker run -p 127.0.0.1:8080:8080 -e ROSTAM_API_KEY=secret ghcr.io/rostamlabs/rostam
Building from source instead is covered in the Quickstart.
A bare :8080 will not start without authentication
With no authenticator configured, every request is served unauthenticated —
so the server refuses to bind a reachable address rather than silently
exposing an open datastore. A bare :8080, 0.0.0.0, :: or any
non-loopback host counts as reachable. To listen beyond loopback, give it
auth (-api-key or -keys-file) or pass -insecure to run open
deliberately. Loopback binds are unaffected, which is why the examples above
spell out 127.0.0.1.
Transports & storage¶
| Flag | Default | Meaning |
|---|---|---|
-http |
:8080 |
REST/JSON listen address; "" disables |
-grpc |
disabled | gRPC listen address |
-tcp |
disabled | binary TCP protocol listen address (what rostam.NewClient speaks) |
-data |
in-memory | persistence directory; empty = nothing survives restart |
-shards |
auto | cache shards (single node) or Raft shards (cluster) |
-ttl-sweep-interval |
30s |
how often each shard reaps expired TTL keys to reclaim memory; 0 disables active reaping (lazy-on-read expiry still applies) |
Any subset of transports can be enabled. GET /v1/health (auth-exempt) is the
liveness probe and GET /v1/ready the readiness probe — use the latter for
load-balancer membership, since health stays green on a node that has lost
quorum. GET /metrics serves Prometheus (Monitoring).
Configuring by environment¶
Every flag has an environment variable: uppercase the name and replace dashes
with underscores, prefixed with ROSTAM_.
| Flag | Variable |
|---|---|
-http |
ROSTAM_HTTP |
-api-key |
ROSTAM_API_KEY |
-pb-auto-failover |
ROSTAM_PB_AUTO_FAILOVER |
-wasm-blob-retention |
ROSTAM_WASM_BLOB_RETENTION |
This is what makes a container or a Kubernetes ConfigMap workable — otherwise 59 flags have to be assembled into one command line.
Precedence is flag > ROSTAM_* > default. An explicit flag always wins,
so a command line can override a baked-in environment without editing it.
Three details worth knowing:
- Empty is a value, not an absence.
ROSTAM_GRPC=""disables the gRPC listener exactly as-grpc ""does. A variable that is set-but-empty is honoured; only an unset variable falls through to the default. - A bad value is fatal.
ROSTAM_SHARDS=sixteenexits withROSTAM_SHARDS="sixteen" is not a valid value for -shards, naming the variable, the value and the flag. It does not silently keep the default — that is how a node ends up running a configuration nobody chose. - Secrets are the deliberate exception. For
-api-keyand-internal-tokenthe environment wins over the flag, because a secret on the command line is visible to other local users through/procand lands in shell history.
Flag reference by area¶
Authentication — -api-key (single superuser key; prefer the
ROSTAM_API_KEY env var so the key doesn't show in /proc), -keys-file
(RBAC key registry), -internal-token (inter-node credential; prefer
ROSTAM_INTERNAL_TOKEN), -audit-log, -tenant-isolation,
-jwt-public-key / -jwt-issuer / -jwt-audience. With no authentication
configured, the server refuses to start on a non-loopback bind unless you pass
-insecure — an open datastore on the network must be a deliberate choice.
→ Security
TLS / mTLS — -tls-cert, -tls-key, -tls-ca,
-tls-require-client-cert, -tls-node-cert, -tls-node-key,
-node-cn-allowlist. One certificate config covers all three transports.
→ Security
Storage — -data, -shards, -config (carries the cache max_memory
stanza), -ttl-sweep-interval, -disable-cold-compaction. The
-disable-cold-compaction flag is an escape hatch, not a tuning knob: a
persistent shard rewrites its pages file live-only at open, and that rewrite is
the only thing that reclaims the bytes left behind by overwritten and expired
keys — without it a shard under TTL churn eventually refuses writes. Turn it off
only to work around a problem with the rewrite itself.
-ttl-sweep-interval (default 30s) sets how often each shard actively reaps
expired TTL keys to reclaim memory, independent of whether they are read again —
an expired key is always returned as not-found regardless, so this is a
memory-reclaim-latency vs CPU-churn tradeoff, not a correctness knob. 0
disables active reaping, leaving only lazy-on-read expiry (and, on persistent
shards, cold compaction at the next restart). Lower it below the default if a
write-heavy replicated node is climbing toward the cache cap between sweeps;
raise it to cut background CPU on a cluster with many shards and slow-churning
TTLs.
The cache eviction knobs (-relocating-eviction, -relocate-reserve-interval,
-in-place-same-size-update, -in-place-seqlock-reads, -sieve-visited-bit)
are also Storage flags, but most of them do nothing on most topologies — see
Cache eviction knobs before enabling one.
Clustering — -cluster, -node-id, -raft-addr, -bootstrap, -peers,
-replication-factor, -persistent-vectors, -reconfigure; durability
posture: -nosync, -volatile-log; replication engine: -replication-mode
(raft | pb, with -min-isr, -pb-addr, -pb-commit-primary,
-pb-auto-failover); Raft transport: -raft-transport
(mux | experimental fabric).
→ Clustering
Backups & cold tier — -backup-dir, -backup-interval,
-backup-prefix, -backup-retention, -backup-bucket, -backup-endpoint,
-backup-region, -backup-tenant, -cold-tier-after, -s3-path-style;
-restore + -allow-missing-shards for one-shot cluster disaster recovery.
→ Backups & cold tier
Help & version — -version prints the build identity and exits: a release
tag for a released binary, otherwise the module pseudo-version Go records, with
a +dirty marker when the tree had uncommitted changes. -h prints the flags
grouped by the areas on this page; -help-all prints every description in full.
Logging — -log-format (text | json), -log-level
(debug–error), -access-log (one structured line per request on every
transport, principal redacted).
Cache eviction knobs¶
Five opt-in flags change how a shard stores rewrites and what it keeps when its
cache reaches max_memory. Out of the box none of them has any effect: the
four switches are off, and -relocate-reserve-interval has a non-zero default
(50ms) that does nothing unless -relocating-eviction is on. Setting one
never turns another on.
They are flags and environment variables only. Each reads its ROSTAM_*
variable (-in-place-same-size-update → ROSTAM_IN_PLACE_SAME_SIZE_UPDATE).
The -config file does not accept them: it carries only settings that have
no flag, and it rejects keys it does not know, so a config file that names one
of these knobs fails to load and the server refuses to start.
| Flag | What it does |
|---|---|
-relocating-eviction |
When a full shard evicts a page, copy the records on it that are still live forward instead of dropping them with the dead versions that share the page. Costs extra copying at eviction time. |
-relocate-reserve-interval |
How often each shard tops up a small reserve of free pages for -relocating-eviction, so a write at capacity finds room instead of evicting inline (default 50ms; 0 turns the reserve off). Every shard runs its own ticker, so a shorter interval costs more on a node with many shards. |
-in-place-same-size-update |
A rewrite of a key whose new value has the same length overwrites the stored copy instead of appending a new one, so steady same-size rewrites stop filling the cache with dead versions. Reads on the shard then take a read lock (unless -in-place-seqlock-reads), and a rewritten key no longer moves to the newest page: on a cache run over capacity, a frequently rewritten key is evicted on the same schedule as a rarely rewritten one. |
-in-place-seqlock-reads |
Keeps reads lock-free under -in-place-same-size-update by validating each read against a version counter after the fact. See the warning below. |
-sieve-visited-bit |
-relocating-eviction rescues only records that were read or rewritten since eviction last passed them, instead of whichever live records it meets first, so a stream of write-once keys cannot push the working set out. |
Where each knob takes effect¶
Check this before enabling a knob: on the wrong topology it is accepted and does nothing.
| Flag | Single node, in-memory (no -data) |
Single node with -data |
-cluster |
|---|---|---|---|
-relocating-eviction |
takes effect (at eviction and in the background reserve) | takes effect (at eviction only) | no effect |
-relocate-reserve-interval |
takes effect with -relocating-eviction |
no effect | no effect |
-in-place-same-size-update |
takes effect | no effect | no effect |
-in-place-seqlock-reads |
takes effect with -in-place-same-size-update |
no effect | no effect |
-sieve-visited-bit |
takes effect with -relocating-eviction |
takes effect with -relocating-eviction |
no effect |
Why:
-cluster: none of them. A cluster shard never evicts: replicas evicting independently would drop different keys and diverge, so replication makes every shard refuse writes at capacity instead. The three eviction knobs (-relocating-eviction,-relocate-reserve-interval,-sieve-visited-bit) therefore never run. The in-place pair acts on every same-size rewrite, full shard or not, but only on a shard that evicts rather than refuses at capacity — and every cluster shard is also file-backed, since a cluster requires-data.-data: no in-place updates. A file-backed page is the durable copy. An overwrite interrupted by a crash is found at recovery as a corrupt entry mid-page, and recovery discards the rest of that page with it — other keys, written long before. An interrupted append normally costs only the write in flight, and the key's previous version is still on disk. So a file-backed shard always appends, and-in-place-seqlock-reads, which exists only to serve in-place updates, has nothing to do either.-data: no background reserve. On a file-backed shard the reserve's copies and the page it frees could reach disk out of order, so a crash could lose a record that would otherwise have survived. Relocation there runs only at eviction time.- The background reserve also needs at least four pages per shard. A small
max_memoryspread over many-shardscan leave fewer, and such a shard keeps no reserve;-relocating-evictionstill works, at eviction time only.
The server does not refuse a knob that has no effect — the same flags or
ROSTAM_* environment shared by a cluster and a single-node box are
over-specified, not broken — but it logs one
warning per such knob at startup, naming the flag, the deployment and the
reason:
level=WARN msg="cache option is set but has no effect on this deployment" component=cache option=InPlaceSameSizeUpdate flag=-in-place-same-size-update deployment="single-node with a data directory" reason="..."
The same warning fires for a pairing that does nothing: -sieve-visited-bit or
-relocate-reserve-interval without -relocating-eviction, and
-in-place-seqlock-reads without -in-place-same-size-update.
-in-place-seqlock-reads is not covered by race detection
This is an opt-in performance trade, not a free speed-up. Its reads are a
deliberate data race: a reader copies page bytes that a writer may be
rewriting at that moment, then discards the copy if a version counter
moved. Go's race detector reports that access by design, and the engine's
concurrent tests for this read protocol are skipped under -race — so a
green race-enabled test run says nothing about it. The protocol's
correctness argument rests on how Go compiles its atomic operations rather
than on a guarantee of the language's memory model (the full argument and
its limits are in cache/seqlock.go). It pays only when reads far
outnumber writes; otherwise leave it off and let reads take the lock.
Embedding the engine directly, the same knobs are fields of
rostam.CacheConfig with the same names in Go form, and NewDirect /
NewEmbedded log the same warnings. A NewEmbedded store with no Peers is
not replicated and behaves as the "single node with -data" column.
Recipes¶
Dev server, everything open, in-memory:
A loopback bind may run open; a network-reachable bind (e.g. -http :8080)
with no authentication is refused at startup unless you pass -insecure.
Single node with persistence and a single API key:
TLS on all transports:
ROSTAM_API_KEY=$(openssl rand -hex 32) rostam-server \
-http :8443 -grpc :9443 -tcp :7443 -data /var/lib/rostam \
-tls-cert server.pem -tls-key server-key.pem
(TLS does not substitute for authentication — a non-loopback bind still needs
an API key, a keys file, or an explicit -insecure.)
Three-node replicated cluster: see Clustering.
Choosing a transport¶
| Transport | Wire cost | Use when |
|---|---|---|
| HTTP | JSON, easiest | curl, Python client, browsers, most integrations |
| gRPC | protobuf | typed clients, streaming-friendly infrastructure |
| TCP | compact binary, lowest latency | the Go smart client (rostam.NewClient), latency-sensitive services |
The TCP event-loop transport (-epoll)¶
-tcp can be served two ways: a goroutine per connection, or an epoll event
loop (-epoll, with -epoll-loops setting the loop count; 0 = GOMAXPROCS).
| Mode | -epoll default |
Why |
|---|---|---|
| Single node | on | It beats goroutine-per-connection under core pressure — up to ~1.4x at low concurrency on an 8-core co-located box — and is within noise on dedicated cores. |
-cluster |
off unless -epoll is passed explicitly |
The event loop executes dispatch inline, so a replicated write blocks the whole loop for a full replication round trip. Write throughput is then capped at ~(loops / RTT) regardless of connection count — measured 2.1x slower than the goroutine server on a real-network 3-node PB RF=2 cluster (61k vs 127k ops/s at 128 connections). |
The event-loop transport is plaintext only: with TLS configured the TCP transport silently falls back to the goroutine server.