This page is the central reference for configuration defaults in Spooky.
Use it when you need to answer two questions quickly:
- which fields may be omitted from the YAML
- what value or behavior Spooky applies when they are omitted
This page reflects the defaults defined in crates/config/src/default.rs and the Default-backed config structs in crates/config/src/config.rs.
When To Use This Page
Use this page when you need to answer:
- what happens if a field is omitted
- whether a behavior is explicit or inherited by default
- which defaults are safe enough to keep for an initial rollout
Use Configuration Reference when you need exact field semantics and validation rules. Use Configuration Examples when you need working end-to-end templates.
How Defaults Work
Spooky applies defaults in three different ways:
- explicit helper-function defaults such as
get_default_port()orperf_default_worker_threads() - struct-level
Defaultimplementations for nested sections such asperformance,observability, andresilience - Rust/Serde zero-value defaults for optional or collection fields such as
false,[],{}, empty strings, andnull
Defaults only apply when a field is omitted. Validation still runs after defaults are applied, so an omitted field may deserialize successfully and still be rejected later if a related feature is enabled.
Examples:
observability.control_api.enableddefaults tofalseobservability.control_api.auth_tokendefaults tonull- if you set
observability.control_api.enabled: true, validation then requiresauth_token
Common Mistakes
- assuming
nulland omission mean the same thing for every nested field - assuming defaults make a section production-safe without reviewing operational implications
- reading defaults without also checking whether a related feature becomes required when enabled
Important Non-Defaults
The following top-level and structural fields are still required and do not have a default:
listenlisten.tlsupstreamupstream.<name>.routeupstream.<name>.backendsupstream.<name>.backends[].idupstream.<name>.backends[].addressload_balancing.typewhen a top-levelload_balancingblock is present
listeners is optional and defaults to [], but if listeners[] is non-empty it overrides the top-level listen block at runtime.
Top-Level Defaults
| Field | Default | Notes |
|---|---|---|
version |
1 |
Current schema version |
listeners |
[] |
Optional multi-listener override |
load_balancing |
null |
Global fallback is absent unless configured |
upstream_tls |
object defaults | See Upstream TLS Defaults |
log |
object defaults | See Log Defaults |
performance |
object defaults | See Performance Defaults |
observability |
object defaults | See Observability Defaults |
resilience |
object defaults | See Resilience Defaults |
security |
object defaults | See Security Defaults |
Listen Defaults
| Field | Default | Notes |
|---|---|---|
listen.protocol |
"http3" |
Native ingress protocol |
listen.port |
9889 |
Default listener port |
listen.address |
"0.0.0.0" |
Bind all IPv4 interfaces |
listen.tls.cert |
"" |
Legacy/default certificate path stays empty until set |
listen.tls.key |
"" |
Legacy/default key path stays empty until set |
listen.tls.certificates |
[] |
Optional SNI certificate map |
listen.tls.client_auth.enabled |
false |
Client certificate auth off by default |
listen.tls.client_auth.require_client_cert |
false |
No client cert requirement by default |
listen.tls.client_auth.ca_file |
null |
No client CA bundle by default |
Upstream TLS Defaults
These defaults apply to the top-level upstream_tls block and to per-upstream upstream.<name>.tls when that nested block is present but fields are omitted.
| Field | Default | Notes |
|---|---|---|
upstream_tls.verify_certificates |
true |
HTTPS upstream verification stays enabled |
upstream_tls.strict_sni |
true |
Upstream SNI stays strict by default |
upstream_tls.ca_file |
null |
No custom CA file |
upstream_tls.ca_dir |
null |
No custom CA directory |
Upstream And Backend Defaults
Upstream-Level Defaults
| Field | Default | Notes |
|---|---|---|
upstream.<name>.load_balancing.type |
"round-robin" |
Applied when the upstream omits load_balancing |
upstream.<name>.load_balancing.key |
null |
No hash/sticky key source by default |
upstream.<name>.host_policy.mode |
pass_through |
Preserve downstream host by default |
upstream.<name>.host_policy.host |
null |
No rewrite target |
upstream.<name>.forwarded_headers.mode |
overwrite |
Spooky rewrites forwarded headers by default |
upstream.<name>.tls |
null |
Uses global upstream_tls unless an override block is set |
Route Defaults
| Field | Default | Notes |
|---|---|---|
upstream.<name>.route.host |
null |
No host matcher unless set |
upstream.<name>.route.path_prefix |
null |
No default path matcher; validation requires either host or path |
upstream.<name>.route.method |
null |
No method restriction |
Backend Defaults
| Field | Default | Notes |
|---|---|---|
upstream.<name>.backends[].weight |
100 |
Equal weight unless overridden |
upstream.<name>.backends[].health_check |
null |
No active health check block unless configured |
Health Check Defaults
These apply when a backend provides a health_check object and omits individual fields.
| Field | Default | Notes |
|---|---|---|
upstream.<name>.backends[].health_check.path |
"/health" |
Default probe path |
upstream.<name>.backends[].health_check.interval |
5000 |
Probe interval in ms |
upstream.<name>.backends[].health_check.timeout_ms |
1000 |
Probe timeout in ms |
upstream.<name>.backends[].health_check.failure_threshold |
3 |
Consecutive failures before unhealthy |
upstream.<name>.backends[].health_check.success_threshold |
2 |
Consecutive successes before healthy |
upstream.<name>.backends[].health_check.cooldown_ms |
5000 |
Cooldown after failure in ms |
Log Defaults
| Field | Default | Notes |
|---|---|---|
log.level |
"info" |
Standard default log verbosity |
log.file.enabled |
false |
Logs go to stderr unless file logging is enabled |
log.file.path |
"/var/log/spooky/spooky.log" |
Used only when file logging is enabled |
log.format |
plain |
Human-readable text output |
Performance Defaults
| Field | Default | Notes |
|---|---|---|
performance.worker_threads |
1 |
Data-plane worker count |
performance.control_plane_threads |
2 |
Tokio control-plane worker count |
performance.packet_shards_per_worker |
1 |
Preserves single-loop packet processing by default |
performance.packet_shard_queue_capacity |
2048 |
Bounded queue depth per shard |
performance.packet_shard_queue_max_bytes |
67108864 |
64 MiB queued-byte cap per shard |
performance.reuseport |
true |
Multi-socket reuse is enabled by default |
performance.pin_workers |
false |
CPU pinning is off by default |
performance.global_inflight_limit |
4096 |
Global request concurrency cap |
performance.per_upstream_inflight_limit |
1024 |
Per-upstream concurrency cap |
performance.inflight_acquire_wait_ms |
0 |
No micro-wait before shedding |
performance.backend_timeout_ms |
2000 |
General backend timeout |
performance.backend_connect_timeout_ms |
500 |
Backend connect timeout |
performance.backend_body_idle_timeout_ms |
2000 |
Backend response-body idle timeout |
performance.backend_body_total_timeout_ms |
30000 |
Backend response-body total timeout |
performance.backend_total_request_timeout_ms |
35000 |
End-to-end backend request timeout |
performance.shutdown_drain_timeout_ms |
5000 |
Shutdown drain budget |
performance.udp_recv_buffer_bytes |
8388608 |
8 MiB UDP receive buffer |
performance.udp_send_buffer_bytes |
8388608 |
8 MiB UDP send buffer |
performance.h2_pool_max_idle_per_backend |
256 |
Idle HTTP/2 connection pool cap per backend |
performance.h2_pool_idle_timeout_ms |
90000 |
Idle HTTP/2 pool timeout |
performance.backend_dns_refresh_enabled |
false |
Hostname backend refresh off by default |
performance.backend_dns_refresh_interval_ms |
30000 |
DNS refresh interval |
performance.per_backend_inflight_limit |
64 |
Per-backend concurrency cap |
performance.new_connections_per_sec |
2000 |
Token-bucket steady refill rate |
performance.new_connections_burst |
500 |
Token-bucket burst size |
performance.max_active_connections |
20000 |
Hard active connection cap |
performance.quic_max_idle_timeout_ms |
5000 |
QUIC idle timeout |
performance.quic_initial_max_data |
10000000 |
QUIC connection flow-control window |
performance.quic_initial_max_stream_data |
1000000 |
QUIC stream flow-control window |
performance.quic_initial_max_streams_bidi |
100 |
Max bidi streams per connection |
performance.quic_initial_max_streams_uni |
100 |
Max uni streams per connection |
performance.max_response_body_bytes |
104857600 |
100 MiB upstream response cap |
performance.max_request_body_bytes |
1000000 |
1 MiB client request-body cap |
performance.request_buffer_global_cap_bytes |
67108864 |
64 MiB worker request-buffer cap |
performance.unknown_length_response_prebuffer_bytes |
2097152 |
2 MiB unknown-length prebuffer cap |
performance.client_body_idle_timeout_ms |
10000 |
Client upload idle timeout |
Resilience Defaults
Adaptive Admission
| Field | Default | Notes |
|---|---|---|
resilience.adaptive_admission.enabled |
true |
Adaptive admission control on by default |
resilience.adaptive_admission.min_limit |
64 |
Minimum inflight ceiling |
resilience.adaptive_admission.max_limit |
null |
No explicit upper bound unless configured |
resilience.adaptive_admission.decrease_step |
16 |
Step down on overload |
resilience.adaptive_admission.increase_step |
16 |
Step up on recovery |
resilience.adaptive_admission.high_latency_ms |
500 |
Latency threshold for pressure signals |
Route Queue
| Field | Default | Notes |
|---|---|---|
resilience.route_queue.default_cap |
512 |
Default per-route queue size |
resilience.route_queue.global_cap |
2048 |
Global route-queue cap |
resilience.route_queue.shed_retry_after_seconds |
1 |
Retry-After hint on shed responses |
resilience.route_queue.caps |
{} |
No per-route overrides by default |
Protocol Policy
| Field | Default | Notes |
|---|---|---|
resilience.protocol.allow_0rtt |
false |
0-RTT disabled by default |
resilience.protocol.allow_connect |
false |
CONNECT disabled by default |
resilience.protocol.early_data_safe_methods |
["GET", "HEAD"] |
Safe methods allowed if 0-RTT is enabled |
resilience.protocol.max_headers_count |
128 |
Header count cap |
resilience.protocol.max_headers_bytes |
16384 |
16 KiB aggregate header budget |
resilience.protocol.enforce_authority_host_match |
true |
:authority and Host must align |
resilience.protocol.allowed_methods |
[] |
Empty means no explicit allowlist |
resilience.protocol.denied_path_prefixes |
[] |
No denied prefixes by default |
resilience.protocol.connect_allowed_ports |
[] |
No CONNECT allowlist entries |
resilience.protocol.connect_allowed_authorities |
[] |
No CONNECT authority allowlist entries |
Circuit Breaker
| Field | Default | Notes |
|---|---|---|
resilience.circuit_breaker.enabled |
true |
Circuit breaker on by default |
resilience.circuit_breaker.failure_threshold |
3 |
Consecutive failures to open |
resilience.circuit_breaker.open_ms |
30000 |
Open interval in ms |
resilience.circuit_breaker.half_open_max_probes |
1 |
Probe requests allowed while half-open |
Hedging
| Field | Default | Notes |
|---|---|---|
resilience.hedging.enabled |
false |
Hedging off by default |
resilience.hedging.delay_ms |
100 |
Delay before hedge attempt |
resilience.hedging.safe_methods |
["GET", "HEAD"] |
Hedge-safe methods |
resilience.hedging.route_allowlist |
[] |
No route allowlist restrictions by default |
Retry Budget
| Field | Default | Notes |
|---|---|---|
resilience.retry_budget.enabled |
true |
Retry budget enforcement on by default |
resilience.retry_budget.ratio_percent |
10 |
Retry budget ratio |
resilience.retry_budget.per_route_ratio_percent |
{} |
No per-route overrides by default |
Brownout
| Field | Default | Notes |
|---|---|---|
resilience.brownout.enabled |
true |
Brownout policy on by default |
resilience.brownout.trigger_inflight_percent |
90 |
Brownout enters near saturation |
resilience.brownout.recover_inflight_percent |
60 |
Brownout exits with headroom |
resilience.brownout.core_routes |
[] |
No explicit core-route list by default |
Distributed Quota
| Field | Default | Notes |
|---|---|---|
resilience.quota.enabled |
false |
Distributed quota is off by default |
resilience.quota.enforcement |
enforce |
Enforced unless explicitly set to shadow |
resilience.quota.backend_failure_policy |
fail_closed |
Backend failures deny by default when quota is enabled |
resilience.quota.backend.kind |
in_memory |
Local in-memory backend is the default base backend |
resilience.quota.backend.key_prefix |
"spooky:quota" |
Default counter key prefix |
resilience.quota.backend.connect_timeout_ms |
250 |
Redis only |
resilience.quota.backend.command_timeout_ms |
100 |
Redis only |
resilience.quota.backend.max_inflight |
1024 |
Redis only |
resilience.quota.local_fallback |
null |
No degraded local fallback unless configured explicitly |
resilience.quota.local_fallback.key_prefix |
"spooky:quota:fallback" |
Default fallback key prefix when fallback is enabled |
resilience.quota.policies |
[] |
Quota stays inert until explicit policies are configured |
Watchdog
| Field | Default | Notes |
|---|---|---|
resilience.watchdog.enabled |
false |
Watchdog off by default |
resilience.watchdog.check_interval_ms |
1000 |
Poll interval |
resilience.watchdog.poll_stall_timeout_ms |
5000 |
Stall threshold |
resilience.watchdog.timeout_error_rate_percent |
60 |
Timeout-rate unhealthy threshold |
resilience.watchdog.min_requests_per_window |
20 |
Minimum request volume before action |
resilience.watchdog.overload_inflight_percent |
95 |
Overload inflight threshold |
resilience.watchdog.unhealthy_consecutive_windows |
3 |
Required consecutive bad windows |
resilience.watchdog.drain_grace_ms |
8000 |
Drain grace before restart |
resilience.watchdog.restart_cooldown_ms |
120000 |
Restart cooldown |
resilience.watchdog.restart_command |
[] |
No structured restart command by default |
resilience.watchdog.restart_hook |
null |
Deprecated legacy hook absent by default |
Observability Defaults
Metrics Endpoint
| Field | Default | Notes |
|---|---|---|
observability.metrics.enabled |
false |
Metrics endpoint disabled by default |
observability.metrics.required |
false |
Startup does not require the metrics endpoint |
observability.metrics.address |
"127.0.0.1" |
Loopback-only by default |
observability.metrics.port |
9901 |
Default metrics port |
observability.metrics.path |
"/metrics" |
Default scrape path |
observability.metrics.max_connections |
512 |
Concurrent metrics connections cap |
observability.metrics.connection_timeout_ms |
30000 |
Metrics connection timeout |
Control API
| Field | Default | Notes |
|---|---|---|
observability.control_api.enabled |
false |
Control API disabled by default |
observability.control_api.required |
false |
Startup does not require the control API |
observability.control_api.address |
"127.0.0.1" |
Loopback-only by default |
observability.control_api.port |
9902 |
Default control API port |
observability.control_api.health_path |
"/health" |
Health endpoint path |
observability.control_api.ready_path |
"/ready" |
Readiness endpoint path |
observability.control_api.runtime_path |
"/admin/runtime" |
Runtime summary path |
observability.control_api.restart_path |
"/admin/runtime/restart" |
Restart control path |
observability.control_api.reload_path |
"/admin/runtime/reload" |
Full config hot-reload path |
observability.control_api.reload_certs_path |
"/admin/runtime/reload-certs" |
Certificate reload path |
observability.control_api.auth_token |
null |
Must be set when the control API is enabled |
observability.control_api.max_connections |
256 |
Concurrent control API connections cap |
observability.control_api.connection_timeout_ms |
30000 |
Control API connection timeout |
Tracing
| Field | Default | Notes |
|---|---|---|
observability.tracing.enabled |
false |
Tracing disabled by default |
observability.tracing.service_name |
"spooky" |
Default OTLP service name |
observability.tracing.otlp_endpoint |
null |
No exporter endpoint by default |
observability.tracing.sample_ratio |
1.0 |
Full sampling if tracing is enabled |
Routing Transparency
| Field | Default | Notes |
|---|---|---|
observability.routing.enabled |
false |
Route-decision reporting disabled by default |
observability.routing.include_reason |
true |
Include decision reasons when routing transparency is enabled |
observability.routing.expose_header |
false |
Response header exposure disabled by default |
observability.routing.header_name |
"x-spooky-route-decision" |
Default transparency header name |
Security Defaults
| Field | Default | Notes |
|---|---|---|
security.privileges.enabled |
true |
Privilege drop is enabled by default |
security.privileges.user |
"nobody" |
Default drop-to user |
security.privileges.group |
"nogroup" |
Default drop-to group |