Skip to content

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.

Disk budget constraints

Three numbers have to be consistent with each other, and the operator refuses the pod if they are not:

yaml
roleOverrides:
  worker:
    resources:
      limits: {ephemeral-storage: 48Gi}
    cacheVolume: {sizeLimit: 32Gi}
yaml
defaultConfig:
  cache.full_bytes: 8GiB
  cache.block_bytes: 2GiB
  • cache.full_bytes + cache.block_bytes must fit inside cacheVolume.sizeLimit.
  • cacheVolume.sizeLimit must not exceed resources.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.

The cache tiers

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_bytes bounds the persistent decoded-block cache on local disk. It is consulted only for remote-block execution and survives a process restart.
  • cache.read_block_bytes bounds 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.

Additional cache budgets

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.

Model cache

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.

Sizing the disk caches

The guidance the storage documentation gives, in order of impact:

  1. Size cache.full_bytes for the complete hot routed working set, not for an average segment, and make sure cache.full_entries is 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.
  2. 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.
  3. Keep active physical segment fanout bounded with scheduled compaction. Cache tuning cannot make an unbounded number of independently planned and scored segments free.

Sizing RAM

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:

promql
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.

Per-role shapes

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.

The builder's memory is a product

builder memory ≈ builder.build_concurrency × builder.sort_memory_bytes

builder.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.

The router's admission budget

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:

promql
yolosearch_ingest_queued_bytes{role=~".*router.*"}
/
yolosearch_ingest_queue_capacity_bytes{role=~".*router.*"}

Storage modes in production

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:

  1. A verified full-cache hit executes locally.
  2. Otherwise the prepared query supplies predicted request count, byte count, and scan fraction, together with measured remote latency and throughput.
  3. 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.
  4. 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:

promql
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.

Compaction

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.

Garbage collection

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.

A worked starting point

For a fleet serving a corpus that does not fit local disk, with moderate ingest:

yaml
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.

Next