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]

KeyTypeDefaultDescription
listen_addrstring"0.0.0.0:8080"Bind address
shutdown_timeout_secsu6425Graceful shutdown timeout (drain BatchWriter + WAL checkpoint). Raise to 60+ for sustained 20K+ rps deployments.
request_timeout_secsu6430Per-request timeout (range 1–300; returns 408 on expiry)
max_connectionsusize10000Maximum 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}/.

KeyTypeDefaultDescription
data_dirstring"./keplor_data"KeplorDB data directory
retention_daysu6490Legacy global GC threshold (0 = disabled). Prefer [retention] tiers.
gc_interval_secsu643600How often GC runs in seconds (0 = disabled). Segment-granular.
wal_checkpoint_secsu64300Rotate active WAL into a sealed segment (0 = disabled)
max_db_size_mbu640Cap data dir size in MB (0 = unlimited). Returns HTTP 507 when exceeded.
size_check_interval_msu641000How long the cached db_size_bytes result is reused before walking every per-tier engine. Range 0–60000 (0 = disable cache).
wal_max_eventsu32500000Events per WAL shard before forced rotation
wal_sync_intervalu3264fsync every N batched writes
wal_sync_bytesu64262144fsync after N buffered bytes
wal_shard_countusize4Per-tier WAL shard count (range 1–64; raise on machines with >8 cores)
mmap_cache_capacityusize256Mmap’d-segment LRU size
rollup_replay_daysu327Days of historical segments replayed into the in-memory rollup store on open
rollup_loop_secsu6460Cadence of the in-process rollup-refresh loop (range 5–3600)

[auth]

KeyTypeDefaultDescription
api_keysstring[][]Simple API keys (id:secret or bare secret). Assigned to default_tier. Empty = open access.
api_key_entriesobject[][]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.

KeyTypeDefaultDescription
default_tierstring"free"Tier assigned to simple-format keys and unauthenticated requests
tiersobject[]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.

KeyTypeDefaultDescription
bucketstringS3 bucket name
endpointstringS3 endpoint URL
regionstringRegion ("auto" for R2, "us-east-1" for AWS)
access_key_idstringAccess key
secret_access_keystringSecret key
prefixstring""Key prefix in bucket (e.g. "events")
path_styleboolfalsePath-style addressing (required for MinIO)
archive_after_daysu6430Archive events older than this many days
archive_after_hoursu640Sub-day archival (hours). Overrides archive_after_days when non-zero. Set to 1 for hourly offload.
archive_threshold_mbu640Also archive when the data dir exceeds this size (MB). 0 = age-only.
archive_batch_sizeusize10000Maximum events per JSONL archive file
archive_interval_secsu643600How 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]

KeyTypeDefaultDescription
allowed_originsstring[][]CORS origin allowlist. Empty = same-origin only. ["*"] = allow all (not recommended).

[pipeline]

KeyTypeDefaultRangeDescription
batch_sizeusize641 – 100,000Events per batch flush
max_body_bytesusize104857601 – 100 MBMax request body size
channel_capacityusize327681 – ∞Batch writer queue depth. Raise for bursty traffic.
flush_interval_msu64501 – 10,000BatchWriter flush cadence. Lower = fresher reads, more segment files. Raise to 250+ for sustained-write workloads where operators tolerate ~half-second read staleness.
write_timeout_secsu64101 – 300Max wait per durable ingest write before returning 500. Bounds worst-case request latency under back-pressure.

[idempotency]

KeyTypeDefaultDescription
enabledbooltrueEnable idempotency key support
ttl_secsu64300Cache TTL for idempotency keys (seconds)
max_entriesusize100000Maximum cached idempotency keys (LRU eviction)

[rate_limit]

KeyTypeDefaultDescription
enabledboolfalseEnable per-key rate limiting
requests_per_secondf64100.0Token bucket refill rate per API key
burstusize200Maximum 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).

KeyTypeDefaultDescription
refresh_interval_secsu6486400Refresh 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_urlstringLiteLLM main branchOverride 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.

KeyTypeDescription
cert_pathstringPath to PEM-encoded certificate chain
key_pathstringPath 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
FlagWhat it doesApproximate gain
RUSTFLAGS=-C target-cpu=nativeAutovectorize for the host’s instruction set (AVX2/AVX-512)5–10%
--features keplor-cli/mimallocReplace the system allocator with mimalloc30–50% under high alloc churn
--features keplor-server/simd-jsonUse simd-json for the ingest body parse1.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