Configuration
TOML config file with environment variable overrides.
Config file
Keplor looks for keplor.toml by default. Override with --config:
$ keplor run --config /etc/keplor/keplor.toml Full example
# keplor.toml
[server]
listen_addr = "0.0.0.0:8080"
shutdown_timeout_secs = 25
request_timeout_secs = 30
max_connections = 10000
[storage]
data_dir = "/var/lib/keplor" # KeplorDB data directory
retention_days = 90 # legacy global GC (prefer [retention] tiers)
gc_interval_secs = 3600 # how often GC runs (0 = disabled)
wal_checkpoint_secs = 300 # rotate active WAL into a sealed segment
max_db_size_mb = 0 # cap data dir size (0 = unlimited)
size_check_interval_ms = 1000 # cache for db_size_bytes (0 = disabled)
wal_max_events = 500000 # events per WAL shard before forced rotation
wal_sync_interval = 64 # fsync every N batched writes
wal_sync_bytes = 262144 # fsync after N buffered bytes
wal_shard_count = 4 # per-tier WAL shards (raise on >8 cores)
mmap_cache_capacity = 256 # mmap'd-segment LRU size
rollup_replay_days = 7 # days of segments replayed on open
rollup_loop_secs = 60 # rollup-refresh cadence
[auth]
api_keys = ["prod-svc:sk-prod-abc123", "staging:sk-staging-def456"]
# Extended format with tier assignment:
# [[auth.api_key_entries]]
# id = "pro-user"
# secret = "sk-pro-key"
# tier = "pro"
[retention]
default_tier = "free"
[[retention.tiers]]
name = "free"
days = 7
[[retention.tiers]]
name = "pro"
days = 90
# [[retention.tiers]]
# name = "team"
# days = 180
[cors]
allowed_origins = [] # empty = same-origin only; ["*"] = allow all
[pipeline]
batch_size = 64
max_body_bytes = 10485760
channel_capacity = 32768
flush_interval_ms = 50 # BatchWriter cadence (1-10000)
write_timeout_secs = 10 # max wait per durable write (1-300)
[idempotency]
enabled = true
ttl_secs = 300
max_entries = 100000
[rate_limit]
enabled = false
requests_per_second = 100.0
burst = 200
[pricing]
refresh_interval_secs = 86400 # 24h. set 0 to disable. range 60-604800.
# source_url = "..." # override only when mirroring through a CDN
# Optional: archive old events to S3/R2 (requires --features s3)
# [archive]
# bucket = "keplor-archive"
# endpoint = "https://<account-id>.r2.cloudflarestorage.com"
# region = "auto"
# access_key_id = "..."
# secret_access_key = "..."
# prefix = "events"
# archive_after_days = 30
# archive_threshold_mb = 500
# archive_batch_size = 10000
# [tls]
# cert_path = "/etc/keplor/cert.pem"
# key_path = "/etc/keplor/key.pem" [server]
| Key | Type | Default | Description |
|---|---|---|---|
listen_addr | string | "0.0.0.0:8080" | Bind address |
shutdown_timeout_secs | u64 | 25 | Graceful shutdown timeout (drain BatchWriter + WAL checkpoint). Raise to 60+ for sustained 20K+ rps deployments. |
request_timeout_secs | u64 | 30 | Per-request timeout (range 1–300; returns 408 on expiry) |
max_connections | usize | 10000 | Maximum concurrent connections. /health and /metrics bypass this so observability survives saturation. |
[storage]
Storage is a KeplorDB data directory — one append-only segment tree per retention tier under {data_dir}/{tier}/.
| Key | Type | Default | Description |
|---|---|---|---|
data_dir | string | "./keplor_data" | KeplorDB data directory |
retention_days | u64 | 90 | Legacy global GC threshold (0 = disabled). Prefer [retention] tiers. |
gc_interval_secs | u64 | 3600 | How often GC runs in seconds (0 = disabled). Segment-granular. |
wal_checkpoint_secs | u64 | 300 | Rotate active WAL into a sealed segment (0 = disabled) |
max_db_size_mb | u64 | 0 | Cap data dir size in MB (0 = unlimited). Returns HTTP 507 when exceeded. |
size_check_interval_ms | u64 | 1000 | How long the cached db_size_bytes result is reused before walking every per-tier engine. Range 0–60000 (0 = disable cache). |
wal_max_events | u32 | 500000 | Events per WAL shard before forced rotation |
wal_sync_interval | u32 | 64 | fsync every N batched writes |
wal_sync_bytes | u64 | 262144 | fsync after N buffered bytes |
wal_shard_count | usize | 4 | Per-tier WAL shard count (range 1–64; raise on machines with >8 cores) |
mmap_cache_capacity | usize | 256 | Mmap’d-segment LRU size |
rollup_replay_days | u32 | 7 | Days of historical segments replayed into the in-memory rollup store on open |
rollup_loop_secs | u64 | 60 | Cadence of the in-process rollup-refresh loop (range 5–3600) |
[auth]
| Key | Type | Default | Description |
|---|---|---|---|
api_keys | string[] | [] | Simple API keys (id:secret or bare secret). Assigned to default_tier. Empty = open access. |
api_key_entries | object[] | [] | Extended keys with explicit tier: { id, secret, tier } |
[retention]
Per-tier retention policies. GC runs one pass per tier, dropping segments whose events are entirely older than the tier’s days threshold.
| Key | Type | Default | Description |
|---|---|---|---|
default_tier | string | "free" | Tier assigned to simple-format keys and unauthenticated requests |
tiers | object[] | free (7d), pro (90d) | Named tiers with retention days. days = 0 keeps events forever. |
Tiers are just names — add "team", "enterprise", or any custom tier via config. No code changes needed.
[archive] (optional, --features s3)
Archive old events to an S3-compatible object store (Cloudflare R2, MinIO, AWS S3) as zstd-compressed JSONL files. Archived events are tombstoned in KeplorDB; segment GC reclaims their disk space on the next sweep. Daily rollups are preserved.
| Key | Type | Default | Description |
|---|---|---|---|
bucket | string | S3 bucket name | |
endpoint | string | S3 endpoint URL | |
region | string | Region ("auto" for R2, "us-east-1" for AWS) | |
access_key_id | string | Access key | |
secret_access_key | string | Secret key | |
prefix | string | "" | Key prefix in bucket (e.g. "events") |
path_style | bool | false | Path-style addressing (required for MinIO) |
archive_after_days | u64 | 30 | Archive events older than this many days |
archive_after_hours | u64 | 0 | Sub-day archival (hours). Overrides archive_after_days when non-zero. Set to 1 for hourly offload. |
archive_threshold_mb | u64 | 0 | Also archive when the data dir exceeds this size (MB). 0 = age-only. |
archive_batch_size | usize | 10000 | Maximum events per JSONL archive file |
archive_interval_secs | u64 | 3600 | How often the archive loop runs (seconds). Default: 1 hour. |
Archival runs every archive_interval_secs (default 1 hour). Events are grouped by (user_id, day), serialized to JSONL, compressed with zstd, and uploaded to S3/R2. S3 connectivity is verified at startup. See Event Archival for the full lifecycle.
Important: Set archive_after_days lower than your shortest retention tier’s days value, or GC will delete events before they can be archived.
[cors]
| Key | Type | Default | Description |
|---|---|---|---|
allowed_origins | string[] | [] | CORS origin allowlist. Empty = same-origin only. ["*"] = allow all (not recommended). |
[pipeline]
| Key | Type | Default | Range | Description |
|---|---|---|---|---|
batch_size | usize | 64 | 1 – 100,000 | Events per batch flush |
max_body_bytes | usize | 10485760 | 1 – 100 MB | Max request body size |
channel_capacity | usize | 32768 | 1 – ∞ | Batch writer queue depth. Raise for bursty traffic. |
flush_interval_ms | u64 | 50 | 1 – 10,000 | BatchWriter flush cadence. Lower = fresher reads, more segment files. Raise to 250+ for sustained-write workloads where operators tolerate ~half-second read staleness. |
write_timeout_secs | u64 | 10 | 1 – 300 | Max wait per durable ingest write before returning 500. Bounds worst-case request latency under back-pressure. |
[idempotency]
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable idempotency key support |
ttl_secs | u64 | 300 | Cache TTL for idempotency keys (seconds) |
max_entries | usize | 100000 | Maximum cached idempotency keys (LRU eviction) |
[rate_limit]
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable per-key rate limiting |
requests_per_second | f64 | 100.0 | Token bucket refill rate per API key |
burst | usize | 200 | Maximum burst size per API key |
[pricing]
Pricing catalog daily refresh task. On error, the existing catalog stays in place (the bundled fallback always loads at startup).
| Key | Type | Default | Description |
|---|---|---|---|
refresh_interval_secs | u64 | 86400 | Refresh cadence in seconds. Range 60–604800. Set 0 to disable. ±10% jitter is applied per cycle so multi-replica deployments don’t stampede the upstream. |
source_url | string | LiteLLM main branch | Override only when mirroring through a CDN / private proxy. |
The keplor_pricing_catalog_age_seconds gauge surfaces silently-stale catalogs to alerting.
[tls]
Optional. When present, the server listens with HTTPS via rustls.
| Key | Type | Description |
|---|---|---|
cert_path | string | Path to PEM-encoded certificate chain |
key_path | string | Path to PEM-encoded private key |
Environment variables
Override any key with KEPLOR_ prefix and underscore nesting:
$ KEPLOR_SERVER_LISTEN_ADDR=0.0.0.0:9090 \
KEPLOR_STORAGE_DATA_DIR=/tmp/keplor_data \
KEPLOR_AUTH_API_KEYS=sk-key1,sk-key2 \
keplor run Environment variables take precedence over the config file.
Build-time perf knobs
For maximum throughput on a known target host:
RUSTFLAGS="-C target-cpu=native" \
cargo build --release -p keplor-cli \
--features keplor-cli/mimalloc \
--features keplor-server/simd-json | Flag | What it does | Approximate gain |
|---|---|---|
RUSTFLAGS=-C target-cpu=native | Autovectorize for the host’s instruction set (AVX2/AVX-512) | 5–10% |
--features keplor-cli/mimalloc | Replace the system allocator with mimalloc | 30–50% under high alloc churn |
--features keplor-server/simd-json | Use simd-json for the ingest body parse | 1.5–3× on 1–5 KB JSON |
| PGO (profile-guided optimization) | 2-stage build with a representative load profile | +10–25% on top, plus ~3× p99 reduction on the f&f path |
For statically-linked deploys also pin --target x86_64-unknown-linux-musl. PGO workflow is documented in docs/operations.md.
Validation
Config is validated at startup. Invalid values produce immediate errors:
Error: invalid config: pipeline.batch_size must be > 0