Deploy
Sizing
Picking cache budgets, local disk, memory and CPU per role, and choosing a storage mode — with the defaults each number starts from and what moves it.
Size each role for its workload. Cache budgets determine local disk and RAM use; query mix, segment count, ingest volume, and object-store latency also affect CPU, network traffic, and request rates.
Three numbers have to be consistent with each other, and the operator refuses the pod if they are not:
roleOverrides:
worker:
resources:
limits: {ephemeral-storage: 48Gi}
cacheVolume: {sizeLimit: 32Gi}defaultConfig:
cache.full_bytes: 8GiB
cache.block_bytes: 2GiBcache.full_bytes + cache.block_bytesmust fit insidecacheVolume.sizeLimit.cacheVolume.sizeLimitmust not exceedresources.limits.ephemeral-storage.
A cache volume larger than the pod's ephemeral-storage limit can let cache
growth trigger eviction before the volume reaches its own limit. A
medium: Memory cache volume is charged against the memory limit instead.
Leave real headroom between the cache budgets and the volume. The ingest spool,
compaction scratch, and /tmp all live on the same node's storage.
Four budgets bound four different things. Two are on disk and survive a restart; two are in RAM and do not.
| Tier | Setting | Default | Lives | Bounds |
|---|---|---|---|---|
| Verified full-segment cache | cache.full_bytes, cache.full_entries |
8 GiB, 1,024 | Disk | Complete verified segment copies |
| Persistent decoded-block cache | cache.block_bytes, cache.block_entries |
2 GiB, 1,048,576 | Disk | Decoded blocks from remote reads |
| WavesDB decoded blocks | cache.read_block_bytes |
512 MiB | RAM | Decoded blocks held by open readers |
| WavesDB table metadata | cache.read_reader_bytes, cache.read_open_readers, cache.read_open_files |
2 GiB, 1,024, 1,024 | RAM | Decoded table indexes and Bloom filters, plus file handles |
The two block-cache budgets govern different tiers:
cache.block_bytesbounds the persistent decoded-block cache on local disk. It is consulted only for remote-block execution and survives a process restart.cache.read_block_bytesbounds decoded blocks retained in process RAM by WavesDB. It serves hydrated and remote readers alike and disappears on restart.
All of these are startup settings. Changing one needs a restart.
Include these caches when accounting for process memory. The query embedding cache is configurable; the other budgets listed here are fixed in code:
| Layer | Budget |
|---|---|
| Query-plan cache | 256 entries per generation |
| Final result cache | 64 MiB / 256 entries, max 4 MiB per entry |
| Per-segment result cache | 256 MiB / 8,192 entries, max 4 MiB per entry |
| Stored projection cache | 64 MiB / 16,384 entries, max 1 MiB per document |
| Query embedding cache | embedding.query_cache_*: 4,096 entries / 64 MiB / 10 minute TTL |
The per-segment result cache is process-lived and keyed by segment commit, so it survives generation swaps; the final result and stored projection caches are generation-local and are discarded with their generation.
embedding.model_cache_bytes defaults to 10 GiB with model_cache_packages: 8,
in its own directory (embedding.model_cache_dir, or models/ under
server.data_dir). Its separate budget prevents model downloads from evicting
segment-cache entries.
If you use internal embedding, that 10 GiB is 10 GiB of disk on top of the cache budgets. If you do not, the directory stays empty.
The guidance the storage documentation gives, in order of impact:
- Size
cache.full_bytesfor the complete hot routed working set, not for an average segment, and make surecache.full_entriesis not the tighter limit. 1,024 entries at 8 GiB is 8 MiB per entry on average; a fleet with large compacted segments hits the byte budget first, a fleet with many small segments hits the entry budget first. - Leave capacity for the block cache and for ingest and compaction scratch in addition to the full cache. They are not carved out of it.
- Keep active physical segment fanout bounded with scheduled compaction. Cache tuning cannot make an unbounded number of independently planned and scored segments free.
cache.read_block_bytes should cover the recurring posting working set, and
cache.read_open_readers should stay above the number of table readers a
normal query touches. Two signals say you got it wrong:
| Symptom | Reading |
|---|---|
| Sustained misses with nearly one eviction per miss | The RAM working set is churning |
| Table evictions, or a resident reader count pinned to its limit | Index and Bloom metadata is reopening |
Both come out of the same family:
yolosearch_storage_wavesdb_read_cache_events_total{cache="block",outcome="eviction"}
/
yolosearch_storage_wavesdb_read_cache_events_total{cache="block",outcome="miss"}yolosearch_storage_wavesdb_read_cache_bytes{cache,state="resident|limit"} and
..._entries{cache="block|table|file",state} give occupancy against the
configured limits directly. The block-entry family has only
state="resident": the decoded-block cache is bounded by bytes, not by an
independent entry count.
Roles have different resource profiles. The operator sample includes worker resource requests; size the other roles for their expected work as well.
| Role | Dominant cost | What drives it |
|---|---|---|
worker |
Local disk and RAM | The cache budgets; segment execution |
coordinator |
Network and CPU | Planning, merge fan-in, result encoding |
merger |
RAM | Buffered frames during hierarchical merge |
aggregator |
Very little | Soft capacity and residency state only |
router |
RAM | ingest.global_queue_bytes admission reservation |
builder |
RAM, CPU, disk | builder.sort_memory_bytes × builder.build_concurrency; the spool |
publisher |
Very little | One leaseholder doing catalog writes |
compactor |
Disk and CPU | compaction.max_output_bytes; opens no query cache |
A dedicated compactor opens no query engine and no full-segment cache, so it does not need the worker's cache budgets at all.
builder memory ≈ builder.build_concurrency × builder.sort_memory_bytesbuilder.sort_memory_bytes defaults to 256 MiB and is the total accounted
budget for one build — one ledger every part of a build charges against. It
divides into the public-ID sorter (an eighth, capped at 64 MiB), the docvalue
and lane spools, the analysis share (builder.analysis_memory_bytes), and the
term partitions, which take what is left.
builder.build_concurrency defaults to one per CPU, bounded at twelve. On a
16-core builder that is 12 × 256 MiB = 3 GiB of builder budget before anything
else. A build that cannot hold its term partitions in the remainder spills to
disk instead, which is the difference between one file per build and hundreds.
builder.max_queued_builds defaults to four times build_concurrency. A
queued build is a spool on disk, not memory, so it costs
ingest.seal_bytes of disk per queued build and buys ingest room across a
build.
| Setting | Default | Meaning |
|---|---|---|
ingest.global_queue_bytes |
256 MiB | Encoded batch bytes this router may reserve across all indexes |
ingest.index_queue_bytes |
64 MiB | Per-index share of the same |
ingest.max_batch_bytes |
4 MiB | One batch |
ingest.max_document_bytes |
16 MiB | One document |
server.max_receive_bytes and server.max_send_bytes both default to 4 MiB,
which is why one record may not exceed an ingest batch: a batch carrying it
could not be delivered.
Watch saturation as a ratio, which is exactly what the shipped
YolosearchRouterSaturated rule does:
yolosearch_ingest_queued_bytes{role=~".*router.*"}
/
yolosearch_ingest_queue_capacity_bytes{role=~".*router.*"}The storage mode decides where an immutable reader gets checkpoint bytes. It never changes query semantics or the segment format.
| Mode | Behavior | Choose it when |
|---|---|---|
AUTO (default) |
Decides per segment; a verified full-cache hit executes locally, otherwise cost estimates and measured throughput pick | Almost always |
REMOTE_BLOCKS |
Stable readers over object storage; bounded range requests on demand | The corpus does not fit local disk, or queries are selective |
HYDRATE_FULL |
Requires a complete verified local copy before executing | The complete routed working set fits local disk and you accept hydration latency before the first query |
AUTO is the default. It can hydrate cold segments in the background:
- A verified full-cache hit executes locally.
- Otherwise the prepared query supplies predicted request count, byte count, and scan fraction, together with measured remote latency and throughput.
- If the policy prefers full hydration but the segment is cold, the interactive request still runs through remote blocks. Full admission is queued on a bounded two-worker, 64-entry background queue.
- Hysteresis and the full-cache budgets prevent a small estimate change from oscillating modes.
Five settings feed the decision, and every one of them is runtime-changeable
with config set:
| Setting | Default | Meaning |
|---|---|---|
storage.remote_latency_floor |
2ms |
Floor under measured remote latency |
storage.remote_throughput_floor |
64MiB |
Floor under measured remote throughput |
storage.hydrate_throughput_floor |
128MiB |
Floor under measured hydration throughput |
storage.scan_threshold_permille |
200 |
Scan fraction above which a read is treated as a scan |
storage.hysteresis_numerator / _denominator |
5 / 4 |
The 5/4 margin a decision must clear to flip |
Watch the decision, not the outcome:
sum by (requested_mode, policy_reason) (
rate(yolosearch_storage_decisions_total[5m])
)Inspect policy_reason alongside the ratio of hydrated to remote requests to
understand why the storage policy changed its choices.
Segment fanout is a capacity input, not only a tidiness concern. Every active segment is planned and scored independently.
| Setting | Default | Meaning |
|---|---|---|
compaction.enabled |
false |
Off by default |
compaction.interval |
1m |
How often the compactor considers work |
compaction.target_bytes |
512MiB |
Target output size |
compaction.max_output_bytes |
4GiB |
Hard cap on one output |
compaction.min_inputs / max_inputs |
4 / 8 |
Segments merged in one job |
compaction.max_active_segments |
16 |
Fanout the compactor works toward |
Run compaction on a dedicated compactor role. It follows latest and
performs single-flight copy-on-write physical merges under these budgets, and
it opens no query engine or full-segment cache, so it does not compete with
workers for cache.
GC is off by default and has two modes.
| Setting | Default | Meaning |
|---|---|---|
gc.sweep_enabled |
false |
Destructive sweeps; off |
gc.interval |
1h |
Sweep cadence |
gc.retained_generations |
2 |
Generations before latest kept reachable |
gc.maximum_stream_lifetime |
1h |
Longest a query stream may still read a generation |
gc.grace |
1h |
Additional safety margin added to every scheduled GC reachability horizon |
gc.minimum_upload_age |
24h |
Minimum age before an unreferenced object can enter a proposal |
gc.quarantine_age |
24h |
Minimum age of an immutable GC proposal before a fresh mark may authorize deleting its still-unreachable keys |
yolosearch catalog gc-dry-run proposes unreferenced objects against explicit
horizons and removes nothing. Run it and read the proposal before enabling
sweeps.
Retention horizons protect uploads and readers:
gc.minimum_upload_age of 24 hours exists because a multipart upload in flight
looks exactly like an unreferenced object, and gc.maximum_stream_lifetime
exists because a long relevance-ordered stream may still be reading a
generation that latest has moved past.
For a fleet serving a corpus that does not fit local disk, with moderate ingest:
spec:
defaultConfig:
storage.mode: AUTO
cache.full_bytes: 8GiB
cache.block_bytes: 4GiB
cache.read_block_bytes: 1GiB
compaction.enabled: "true"
roleOverrides:
worker:
resources:
requests: {cpu: "4", memory: 12Gi}
limits: {memory: 24Gi, ephemeral-storage: 64Gi}
cacheVolume: {sizeLimit: 40Gi}
builder:
resources:
requests: {cpu: "4", memory: 8Gi}
limits: {memory: 16Gi, ephemeral-storage: 32Gi}
compactor:
resources:
requests: {cpu: "2", memory: 8Gi}
limits: {memory: 16Gi, ephemeral-storage: 32Gi}Then measure, in this order: the storage-decision reasons, the WavesDB block-cache miss and eviction rates, the full-cache occupancy against its budget, and publication lag. Move one budget at a time.
- Observability — the series these recommendations read
- Configuration reference — every setting, with scope and default
- Storage modes and caching — the mechanism behind the budgets