tephra-server 0.4.1

Synchronous, thread-per-connection TCP server exposing a tephra event store over the length-prefixed protobuf protocol
# Example tephra-server configuration.
#
# Every value shown is the built-in default, so an empty file behaves identically to running
# with no config at all. Pass this file with `--config tephra.example.toml`. Any key can also
# be set from the environment as TEPHRA__<SECTION>__<KEY>, upper-cased, e.g.
# TEPHRA__WRITER__MAX_BATCH_BYTES=16777216. The command line only carries --bind, --data-dir,
# and --log, and those override whatever is set here.

# Address the TCP listener binds.
bind = "127.0.0.1:9000"
# Directory holding the log (and index) segment files.
data_dir = "tephra-data"
# Tracing filter. Omit to fall back to TEPHRA_LOG, then to "info".
# log = "tephra=debug"

[segment]
# Total size of each segment file in bytes, including its header.
size = 268435456           # 256 MiB

[writer]
# Bounded request-queue depth. When full, an append blocks (backpressure). Keep it well above
# max_batch_records or it caps the achievable batch size.
queue_capacity = 16384
# Most requests folded into one group-committed batch. The main throughput-vs-latency dial.
max_batch_records = 2048
# Byte budget for one batch. Clamped down to the segment capacity at startup, so it can never
# exceed a shrunk segment.size.
max_batch_bytes = 8388608  # 8 MiB
# Recent-position window width for the durable tips map (a memory bound only).
tips_window = 1000000
# Resolve the append-condition durable arm with the log scan instead of the index existence
# check. An operational escape hatch: the log is the source of truth, so the scan is always
# safe, just slower.
condition_force_scan = false

[read]
# The planner's K: the index is chosen only when the post-pruning range is at least scan_bias
# times the estimated result count, so larger values bias toward scanning at the margin. This
# only changes which correct path runs, never the answer.
scan_bias = 4

[server]
# Largest single frame accepted or produced, in bytes.
max_frame_len = 16777216   # 16 MiB

[server.reads]
# A streamed read (or subscription) is flushed as a frame once it holds this many events.
batch_events = 1024
# ...or once its buffered events reach this many bytes.
batch_bytes = 524288       # 512 KiB
# Reusable worker threads in the shared read pool. 0 = auto (one per logical CPU). Warm reads are
# short and CPU-bound, so one per core reaches the read-parallelism ceiling; raise it for
# slow-client streaming-read workloads.
worker_threads = 0

[server.subscriptions]
# How often an idle subscription's blocking wait wakes to re-check server shutdown, in
# milliseconds. Keeps a subscription with no events flowing responsive to shutdown without a
# heartbeat frame.
wait_tick_ms = 250
# Most live subscriptions a single connection may hold at once; one over the limit is rejected.
max_concurrent = 64

[server.backpressure]
# Most appends + reads a single connection may have in flight at once. Once reached, the reader
# blocks (backpressure) until one finishes, bounding read threads and the append-reply backlog.
max_inflight_per_conn = 256
# Depth of a connection's outbound bulk frame queue (read/subscription frames buffered before
# backpressure). Small control frames (acks, stats, errors) use a separate priority lane.
frame_queue_depth = 256

[server.limits]
# Most connections served at once, across all clients. Each connection costs a few OS threads, so
# this caps total server resources independent of any per-connection budget. A connection over the
# cap is closed immediately, before a request is read. 0 = unlimited (an explicit opt-out).
max_connections = 1024

[server.keepalive]
# TCP keepalive idle time before the first probe on an accepted connection, in seconds. The OS
# default (~2h on Linux) is too long to reap a silently-dead subscription promptly.
idle_secs = 60
# Interval between TCP keepalive probes once they start, in seconds.
interval_secs = 15

[server.timeouts]
# Seconds a partial request frame may take to finish once its first byte arrives, before the
# connection is reaped. Defends against a slow-loris trickle (which a per-read socket timeout misses,
# since it resets on every byte). Only ever touches a partial frame in flight, so a connection idling
# silently at a frame boundary is unaffected. 0 disables it.
incomplete_frame_secs = 30
# Seconds a freshly accepted connection may take to send its first complete frame before being
# reaped. 0 disables it (the default). Off by default because a pooling client (such as the async
# client's idle bulk sockets) legitimately opens a connection and sends nothing until its first read;
# enable it only where clients do not hold connections open before using them.
handshake_secs = 0
# Seconds a connection with no request in flight and no live subscription may sit idle before being
# reaped. 0 disables it (the default), for the same pooling reason as handshake_secs. Subscriptions
# and in-flight requests count as activity, so a long-lived subscription is never reaped.
idle_secs = 0

[metrics]
# Address for the Prometheus /metrics HTTP endpoint, on its own port. Omitted (disabled) by
# default; set it to expose metrics, e.g. for scraping from an internal network only.
# bind = "127.0.0.1:9100"

[tls]
# Server-authenticated TLS. Set both to a PEM certificate chain and its private key to serve TLS;
# omit both (the default) to serve plaintext. Setting only one is a startup error.
# cert = "server.crt"
# key = "server.key"

[auth]
# Bearer-token authentication. With no tokens (the default) the server is open. Add one table per
# accepted token; any of them authenticates a connection in its opening Hello. List several to
# rotate without downtime: add the new token, roll clients over, then remove the old.
# [[auth.tokens]]
# token = "a-long-random-secret"
# [[auth.tokens]]
# token = "the-next-secret-during-rotation"
#
# Tokens are secrets, so by default they require TLS. Set this to permit them over a plaintext
# listener, for a deployment that terminates TLS at a proxy or mesh in front of tephra.
# allow_insecure = false