bouine is configured via a YAML file passed with --config. Environment variable interpolation is supported: ${VAR} is replaced with the value of VAR, and ${VAR:-default} provides a fallback. $$ escapes to a literal $.
values.yaml keys with defaults.listen:
http: ":8080"
admin: ":9000"
storage:
hot_max_bytes: 256MiB
upstream_pools:
- name: app
targets: ["app.default.svc:8080"]
routes:
- match: { path_prefix: / }
pool: app
cache:
ttl_default: 60slisten:
http: ":80"
https: ":443"
admin: ":9000"
cluster: ":8443"
tls:
certs:
- cert_file: /etc/bouine/tls/cert.pem
key_file: /etc/bouine/tls/key.pem
sni: ["example.com", "*.example.com"]
min_version: "1.2"
storage:
hot_max_bytes: 2GiB
warm_dir: /var/lib/bouine
warm_max_bytes: 50GiB
cluster:
join:
- "bouine-0.bouine-headless.ns.svc.cluster.local:8443"
- "bouine-1.bouine-headless.ns.svc.cluster.local:8443"
- "bouine-2.bouine-headless.ns.svc.cluster.local:8443"
hop_limit: 2
upstream_pools:
- name: api
targets: [api.default.svc:8080]
tls:
enabled: false
health:
active:
path: /healthz
interval: 5s
timeout: 1s
unhealthy_threshold: 3
passive:
consecutive_5xx: 5
connect:
timeout: 10s
keep_alive: 15s
routes:
- match: { host: "api.example.com", path_prefix: /v1/ }
pool: api
cache:
ttl_default: 60s
stale_while_revalidate: 30s
stale_if_error: 300s
negative_ttl: 5s
jitter_percent: 10
refresh_before_expiry: true
refresh_margin_percent: 20
refresh_timeout: 5s
refresh_concurrency: 16
refresh_min_hits: 3
refresh_persist_cycles: 2
refresh_min_score: 1048576
refresh_max_rps: 100
refresh_reactive_first: true
key:
include_headers:
- Accept-Language
exclude_headers:
- x-request-id
- x-trace-id
- x-forwarded-forlisten| Field | Default | Description |
|---|---|---|
http | ":80" | HTTP/1.1 plaintext listener |
https | "" | HTTPS (TLS) listener. See TLS. |
admin | ":9000" | Admin API (health, metrics, purge) |
cluster | "" | Gossip cluster port |
max_connections | 0 | Max concurrent data-plane connections (0 = default 4096; Helm chart sets 4096 default / 8192 production / 16384 HA). Protects against FD exhaustion. Idle keep-alive connections hold a slot. |
idle_timeout | 120s | Keep-alive idle timeout for data-plane connections: how long a connection with no in-flight request stays open. Also used by the H1 fast-path parser, so the two stay in sync. With an upstream proxy or LB in front, keep its keep-alive idle timeout below this value so it closes idle connections first — otherwise bouine can close a connection mid-reuse and the upstream logs upstream prematurely closed connection. |
tcp_fast_open | true (Linux) | Enable TCP_FASTOPEN on data-plane listeners. Defaults to true on Linux, no-op elsewhere. |
tcp_defer_accept | true (Linux) | Enable TCP_DEFER_ACCEPT on data-plane listeners. Defaults to true on Linux, no-op elsewhere. |
reuse_port | true (Linux) | Enable SO_REUSEPORT on data-plane listeners (N parallel accept loops). Defaults to true on Linux, false on other platforms. |
tcp_quickack | true (Linux) | Enable TCP_QUICKACK on accepted data-plane connections to reduce latency by avoiding delayed ACKs. Defaults to true on Linux, no-op elsewhere. |
read_timeout | 30s | Bounds how long reading a single request’s header and body may take, per request (since v0.5.9). This is the slowloris defense for clients that drip-feed bytes — it is not an end-to-end request deadline (origin fetches are bounded by fetch_timeout). Raise it for slow mobile clients or large uploads. Must stay below the 5-minute data-plane safety-net write timeout. |
storage| Field | Default | Description |
|---|---|---|
hot_max_bytes | — | RAM cache size. See
size units. Example: 2GiB. |
eviction_algorithm | sieve | Eviction policy for both tiers: sieve (visited-bit sweep) or cachaner (SIEVE + 3-bit frequency counter — up to 7 second chances for hot objects). Per-tier overrides: hot_eviction_algorithm, warm_eviction_algorithm. |
hot_mmap_slab | false | Use mmap slab allocator for hot body bytes (reduces GC pressure, Linux only) |
warm_dir | "" | Path for mmap warm-tier segments. Empty disables. See Storage tiers. |
warm_max_bytes | "" | Max warm-tier disk usage |
warm_max_entries | — (auto) | Max warm-tier entry count. Auto-derived from GOMEMLIMIT when unset. |
warm_max_disk_bytes | "" | Max total warm-tier disk usage (all segments) |
min_free_disk | "" | Minimum free disk space before warm writes are paused |
warm_preallocate | 0 | Preallocate warm-tier segment files totaling this size at startup. Eliminates disk amplification from append-only segments. Zero = create on demand. |
compact_interval | 30m | Interval between warm-tier compaction sweeps. Set to -1 to disable periodic compaction (not recommended). |
body_threshold | 64KiB | Body size threshold for warm-tier admission. Objects larger than this are written to warm on every Put; smaller objects only by the background sync loop. |
warm_sync_interval | 60s | Interval between hot-to-warm sync batches |
warm_sync_batch_size | 5000 | Max objects per warm sync batch |
wal_sync_interval | 100ms | WAL fsync interval (async batching) |
compact_startup_delay | 5m | Delay before first compaction on startup. Prevents I/O contention with WAL replay and cluster join. Set to -1 to start immediately. |
checkpoint_interval | 5m | Warm-tier checkpoint interval |
checkpoint_wal_threshold | 100000 | WAL entry count that triggers a checkpoint, regardless of interval. Bounds WAL replay time on unclean restart. |
segment_cache_size | 0 (auto) | Number of warm-tier segment files to keep mmap-ed. 0 = auto (min(segCount, 256)). -1 = unlimited (no eviction). |
tombstone_queue_size | 65536 | Tombstone queue depth for warm-tier deletions. Increasing this reduces drops under bursty eviction pressure. |
tombstone_drain_interval | 1s | Interval between tombstone drain sweeps. Set to -1 to disable the dedicated drain goroutine. |
cluster| Field | Default | Description |
|---|---|---|
mode | strong | Consistency mode: strong or eventual. The cluster is enabled when listen.cluster is set. See
Clustering. |
join | [] | Seed addresses (StatefulSet pod DNS) |
hop_limit | 2 | Max peer-fetch hops before origin fallback (strong mode only) |
peer_max_conns_per_host | 8 | Pipelined peer connections per peer. Default 8, with 16 pending requests each, gives 128 concurrent peer fetches per peer. Set to 1 to disable pipelining. |
peer_max_idle_conn_duration | 120s | How long idle peer RPC connections are kept before closing. Must stay below admin.idle_timeout (default 300s) — config validation rejects any explicit value that violates the ordering, because a peer request sent on a connection the admin server already reaped fails with EOF and falls back to origin. |
peer_fetch_concurrency | 4 | Bounds concurrent peer-fetch and peer-put RPCs per node (since v0.5.11, range 1–128). In strong mode most cache hits are peer hits, so this semaphore sits on the hot path; raise it together with peer_max_conns_per_host under load to cut peer-hit tail latency. |
ban_ttl | 24h | How long a lazy invalidation ban stays in the active ban list before the reaper prunes it (since v0.5.20, must be ≥ 1s when set). RFC 9111 §4.4 exempts objects stored after the ban, so cache-lifecycle surrogate invalidations are safe at minutes scale — lower it to bound the hit-ratio damage of an over-broad ban. |
join_timeout | 120s | Max time to wait for cluster join. In strong mode, the pod stays not-ready if join fails. In eventual mode, the pod becomes ready and retries in the background. |
handoff_queue_depth | 4096 | Memberlist per-peer message buffer. Absorbs bursts of cache invalidations. Negative values are rejected. |
tls.ca_bundle | "" | CA certificate path for peer-to-peer mTLS. Empty = plain HTTP. |
tls.cert_file | "" | Client certificate for mTLS |
tls.key_file | "" | Client private key for mTLS |
routes[]Route matching uses host, path_prefix, and optionally methods. Routes are matched in declaration order; the first match wins. Regex-based path matching is not supported in routes — use path_regex in
ban predicates for invalidation.
A route must specify exactly one of pool or static.root. The former proxies to an upstream pool; the latter serves files from a local directory. See
Static file serving.
| Field | Default | Description |
|---|---|---|
name | "" | Human-readable label used in Prometheus route label and the dashboard. Defaults to host:path_prefix when empty. |
match.host | "" | Match on Host header (empty = any) |
match.path_prefix | "" | Match on URL path prefix (empty = any) |
match.methods | [] | Restrict to listed HTTP methods, e.g. [GET, HEAD]. Empty = all methods. Normalised to upper-case. Lets you give GET and POST on the same path independent cache policies. |
pool | — | Upstream pool name. Required unless static.root is set. |
static.root | "" | Absolute path to a directory to serve files from. Required unless pool is set. See
Static file serving. |
static.index | [] | Index files to try (in order) when the request path maps to a directory, e.g. [index.html]. |
static.max_file_size | 10MiB | Per-file size cap. Files larger than this are rejected with 413. |
routes[].request| Field | Default | Description |
|---|---|---|
header_set | {} | Headers to set on the upstream request. Rewrites apply on every origin fetch (miss, revalidation, invalidating methods, background refresh) — enforced on all emit paths since v0.5.19. |
header_remove | [] | Headers to remove from the upstream request |
strip_prefix | "" | Strip this path prefix before forwarding to the upstream (e.g. /api/v1/users → /users). Must start with /. The cache key still uses the original path. |
routes[].cache| Field | Default | Description |
|---|---|---|
enabled | true | Set to false to bypass caching for this route |
ttl_default | 0 | Default TTL when origin has no Cache-Control |
ttl_override | 0 | Force bouine’s internal TTL regardless of upstream Cache-Control/Expires; upstream headers are forwarded unaltered. See
TTL override. |
stale_while_revalidate | 0 | Serve stale while refreshing in background |
stale_if_error | 0 | Serve stale on origin 5xx |
negative_ttl | 0 | Cache 404/405/410/501 responses for this duration |
jitter_percent | 0 | Random ±N% on TTLs to prevent stampedes (0–50) |
stayin_alive | false | Serve stale indefinitely when upstream is down (see Stayin Alive) |
allow_set_cookie | false | Allow caching responses that carry Set-Cookie. Default blocks caching such responses (nginx-style). When true, the response is cached but Set-Cookie is stripped from the stored copy. See
Set-Cookie caching. |
max_object_size | 0 | Skip caching responses whose body exceeds this size (e.g. 1MiB). The response is still proxied. 0 = no limit. |
max_response_bytes | 64MiB | Hard cap on bytes buffered per origin fetch. Aborts the fetch (502) when exceeded. Different from max_object_size which controls caching eligibility. Default derives from GOMEMLIMIT (7%) or a built-in 64 MiB floor. |
max_fetch_concurrency | 32 | Max concurrent foreground origin fetches per route (collapsed via singleflight). Excess requests wait up to fetch_wait_timeout for a slot, then shed. |
fetch_timeout | inherits connect.response_header_timeout (30s) | The authoritative per-route origin timeout (header + body), starting once a fetch slot is acquired (semantics since v0.5.11). When unset, the route inherits the pool’s connect.response_header_timeout; when set, the value is enforced verbatim in either direction — a route may exceed the pool-wide knob to give a slow endpoint more time without raising the wait for every other route. Must stay below the 5-minute data-plane safety-net write timeout. |
fetch_wait_timeout | 100ms | How long a foreground miss waits for an origin-fetch slot (bounded by max_fetch_concurrency) before shedding: stale object served if one is in scope, otherwise 503 + Retry-After: 1. Validated range: 0–1s. Independent of fetch_timeout. See
Streaming and live responses. |
max_streaming_buffer_bytes | auto (GOMEMLIMIT × 7%, floor 64MiB) | Total bytes held in live streaming tee buffers across concurrent miss-fetches on this route. When exceeded, new cacheable misses fall back to synchronous buffering instead of streaming. |
refresh_before_expiry | false | Enable proactive background conditional revalidation before TTL expiry. See Refresh before expiry. |
refresh_margin_percent | 10 | Percentage of TTL before expiry at which the background refresh fires (1–50). E.g. 20 fires at 80% of TTL. |
refresh_timeout | 10s | Maximum duration for a single background refresh fetch (5s–120s) |
refresh_concurrency | 8 | Maximum concurrent background refresh fetches per route (1–64) |
refresh_min_hits | 0 | Minimum cache hits during a TTL window for an object to qualify for re-scheduling after a refresh. 0 disables the gate. See
Popularity gates. |
refresh_persist_cycles | 0 | Additional TTL cycles to keep refreshing after the popularity gate would block. Requires refresh_min_hits > 0. See
Persist cycles. |
refresh_min_score | 0 | Minimum refresh priority score (staleHits × bodySize) for re-scheduling. Requires refresh_min_hits > 0. See
Popularity gates. |
refresh_max_rps | 0 | Caps background refresh fetches per second per route (0 or 1–10000). 0 = no limit. See
Rate limiting. |
refresh_reactive_first | false | SWR-first mode: new objects rely on stale-while-revalidate instead of proactive refresh. Requires stale_while_revalidate > 0 and refresh_min_hits > 0. See
Reactive-first mode. |
routes[].cache.key| Field | Default | Description |
|---|---|---|
include_headers | [] | Headers to include in cache key (replaces Vary) |
exclude_headers | [] | Request header names to strip from the Vary-based variant key, preventing cache fragmentation from per-request headers like X-Request-Id. Matched case-insensitively. See
Excluding headers. |
strip_query_params | [] | Query parameter names to exclude from the cache key, e.g. [utm_source, fbclid]. The params are still forwarded to the upstream. See
Stripping query parameters. |
keep_query_params | [] | When non-empty, restricts the cache key to only these query parameters; all others are excluded. Mutually exclusive with strip_query_params and strip_query_prefix. Equivalent to Varnish qs.keep(). |
strip_query_prefix | [] | Strip query params whose names start with any of these prefixes (e.g. [utm_, fb_, _ga]). Covers wildcard stripping without enumerating every variant. Capped at 16 entries. |
strip_empty_params | false | Remove query params with empty values (?foo=&bar=1 → ?bar=1). Does not apply to params in keep_query_params. |
dedup_query_params | false | Keep only the first value for duplicate query params (?a=2&a=1 → ?a=2). Values are not sorted. |
canonicalize_pathwas removed in v0.5.19. The knob was parsed and documented but its listener-level wiring never landed, so it had no effect; configs setting it now fail at load time with the strict loader. Remove the key from your config when upgrading.
upstream_pools[].connectAll fields are optional; a zero/empty value applies the built-in default, so existing configs keep their current behaviour.
| Field | Default | Description |
|---|---|---|
timeout | 10s | TCP dial timeout |
keep_alive | 30s | TCP keep-alive probe interval on origin connections |
max_connections | 64 | Max concurrent connections per origin host (fasthttp MaxConnsPerHost). Per host, not per pool: a pool with N targets gets N × max_connections. Bounds FD consumption under slow origins. |
max_idle_conn_duration | 90s | How long an idle pooled origin connection is kept before closing. Keep this below any LB idle timeout between bouine and the origin (e.g. AWS NLB 350s) so bouine closes idle connections first. |
response_header_timeout | 30s | Max time to wait for response headers from upstream. Zero applies a 30s built-in default. Primary defence against slow-origin resource exhaustion — and, since v0.5.11, the default origin timeout (header + body) for every route on the pool that does not set its own cache.fetch_timeout. |
hedge_timeout | 0 (disabled) | Hedged fetch (since v0.5.19): fire a duplicate request to the same pool when the primary does not respond within this duration; the first response wins. Only applies to idempotent methods (GET, HEAD, OPTIONS). Zero disables hedging. See Hedged fetch. |
Active health probes the upstream periodically. Passive ejects after consecutive failures.
health:
active:
path: /healthz
method: GET # default
interval: 5s
timeout: 1s
healthy_threshold: 1
unhealthy_threshold: 3
expected_status_codes: [200]
passive:
consecutive_5xx: 5
eject_for: 30spassive.eject_for restores passively ejected targets once the window
elapses and re-ejects them automatically if they are still broken
(enforced since v0.5.19; restores are counted in the
origin_restores_total metric with a source label).
admin| Field | Default | Description |
|---|---|---|
token | "" (auto-generated) | Admin bearer token. See Authentication. |
max_batch_size | 1000 | Max URLs per /v1/purge/batch request |
idle_timeout | 300s | Keep-alive idle timeout for admin-server connections, including cluster peer RPCs (/v1/peer/*). Peer clients default to a 120s idle duration, so they close idle connections before the admin server reaps them; keep cluster.peer_max_idle_conn_duration below this value. |
rate_limit_per_second | 0 | Rate limit on admin write endpoints (0 = no limit) |
pprof_enabled | false | Enable /debug/pprof/* profiling endpoints |
drain_duration | 10s | Duration the /drain endpoint blocks during shutdown (K8s preStop hook) |
tracingConfigure OpenTelemetry span export. Leave endpoint empty (default) to disable tracing.
| Field | Default | Description |
|---|---|---|
endpoint | "" | OTLP/HTTP collector URL, e.g. http://otel-collector:4318. Empty disables. |
service_name | "bouine" | service.name OTel resource attribute |
sampling_rate | 1.0 | Fraction of requests to sample (0.0–1.0) |
cloudflareOptional Cloudflare Cache API propagation. See Cloudflare CDN propagation for full details and Kubernetes secret wiring.
| Field | Default | Description |
|---|---|---|
zone_id | "" | Cloudflare zone identifier (non-secret) |
api_token | "" | Cache Purge API token. Prefer CF_API_TOKEN env var. |
api_tokens | [] | Additional API tokens for rate-limit spreading (also injectable via CF_API_TOKENS, comma-separated). The client rotates across all tokens to multiply the effective rate-limit budget. |
async | true | Return immediately; CF call runs in background goroutine |
timeout | 10s | Per-call timeout for CF API requests |
batch.max_batch_size | 0 (passthrough) | Max items coalesced per CF API call when > 0. Purges are deduplicated and batched. |
batch.max_wait | 500ms | Max time a batched item waits before a flush |
circuit.enabled | false | Circuit breaker: fail fast during CF API outages |
circuit.failure_threshold | 5 | Consecutive failures before the circuit opens |
circuit.open_timeout | 30s | Time before probing again |
circuit.half_open_max_calls | 1 | Probe calls allowed in half-open state |
retry.enabled | false | Dead-letter queue for failed CF purges: retried with exponential backoff so transient CF outages don’t lose invalidations |
retry.max_queue_size | 1000 | Max items in the retry queue; new failed items are dropped when full |
retry.max_retries | 3 | Retry attempts per item |
retry.base_delay | 1s | Initial retry delay (grows exponentially) |
retry.max_delay | 30s | Retry backoff cap |
propagate.purge | true | Forward POST /v1/purge to CF PurgeSingleFile |
propagate.ban | true | Forward POST /v1/ban to CF (tags / prefixes / hostnames) |
propagate.refresh | true | Forward POST /v1/refresh to CF PurgeSingleFile |
experimentalOpt-in features that are not yet stable. All fields default to off. See Experimental features.
| Field | Default | Description |
|---|---|---|
h1_fast_path | false | Enable custom HTTP/1.1 parser for zero-allocation cache hits. Eliminates *http.Request and http.ResponseWriter construction on the hit path (~40% CPU reduction, 0 allocations). Misses and non-GET/HEAD requests fall through to the standard fasthttp handler. See
Experimental features. |
h1_reactor | false | Enable the single-goroutine epoll event loop that batch-serves cache hits without per-request goroutine park/unpark (Linux only; requires h1_fast_path). See
Experimental features. |
h1_fast_peer_path | false | Serve peer-fetched objects directly on the H1 fast path without falling through to the slow path (since v0.5.20; requires h1_fast_path and a strong-mode cluster; not wired under h1_reactor). See
Experimental features. |
| Field | Default | Description |
|---|---|---|
gogc | 100 | Go GC percentage. Set to -1 to disable percentage-based GC, relying solely on GOMEMLIMIT. |
url_ring_sample_rate | 0 | 1-in-N sampling for the dashboard URL ring buffer. 0 = record every non-HIT request. 100 = 1 in 100 (reduces sync.Map overhead under high miss rates). 1 = record every call (debug mode). |
Set upstream_pools[].connect.hedge_timeout to hedge slow origins (wired
into the fetch path since v0.5.19). When the primary request does not get a
response within hedge_timeout, bouine fires a duplicate request to the
same pool and returns whichever response arrives first. Only idempotent
methods (GET, HEAD, OPTIONS) are hedged — SSE and non-idempotent
requests never duplicate. Zero (the default) disables hedging.
upstream_pools:
- name: api
targets: [api.default.svc:8080]
connect:
hedge_timeout: 250msAll byte-size fields (hot_max_bytes, warm_max_bytes) accept any of these suffixes (case-insensitive):
| Suffix | Multiplier | Family |
|---|---|---|
B | 1 | exact |
K, KB | 10³ | SI decimal |
KiB, KI | 1 024 | IEC binary |
Ko | 10³ | French SI |
M, MB | 10⁶ | SI decimal |
MiB, MI | 1 048 576 | IEC binary |
Mo | 10⁶ | French SI |
G, GB | 10⁹ | SI decimal |
GiB, GI | 1 073 741 824 | IEC binary |
Go | 10⁹ | French SI |
T, TB | 10¹² | SI decimal |
TiB, TI | 2⁴⁰ | IEC binary |
To | 10¹² | French SI |
Recommendation: Use IEC binary units (
MiB,GiB) for clarity. The Helm chart defaults useGiB.
bouine does not support live config reload. All config changes require a process restart. On Kubernetes, use a rolling restart:
kubectl rollout restart statefulset/bouineFor TLS certificate rotation, restart the process or use Kubernetes rolling restarts with cert-manager.