Skip to content

docs(memory): sizing model, coordinated budget, and how to validate it - #2079

Merged
lukekim merged 3 commits into
trunkfrom
docs/memory-sizing-and-tuning-guidance
Aug 11, 2026
Merged

docs(memory): sizing model, coordinated budget, and how to validate it#2079
lukekim merged 3 commits into
trunkfrom
docs/memory-sizing-and-tuning-guidance

Conversation

@lukekim

@lukekim lukekim commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Why

The memory guide gave sizing rules as multiples of dataset size plus a minimum RAM figure, with no account of the fixed cost those multiples sit on top of. That reads as memory scales with data, which makes a proportionally downscaled staging environment look like a valid model of production. It is not one, and nothing in the docs warned anyone off it.

Prompted by a support conversation about a deployment being OOM-killed in a small container; the guidance here is written generally, and every number is checked against the shipped runtime.

The reframing

Total = Baseline + Working Set. The baseline is a function of dataset count, not data volume — each Cayenne table holds a segment cache (1/128 of memory, clamped to 256 MB–1 GB), and a CDC table adds a PK keyset cache (1/32, clamped 256 MiB–8 GiB) and a coalesce buffer.

The clamps are the point: the per-dataset cost stops shrinking once the environment is small, so it is a rounding error at production volumes and the dominant term below them. A worked table shows the same ten-dataset Spicepod at ~76% baseline in 4 GiB and ~9% in 32 GiB. The existing multiples are relabelled as working-set figures rather than totals.

Four things from the runtime the docs did not carry

Each of these changes what an operator should actually do:

  • How the Runtime Partitions Memory — the 70/20/10 partition (pools / in-memory CDC tier / off-pool headroom), the compaction carve out of the query pool, and the 50% floor.
  • Tuning the Memory Limit Safely — the limit is bounded on both sides when CDC is active, and both directions are counterintuitive. Lowering it need not reduce resident memory: the tier is sized from what is left after the pools and floats up to a quarter of memory to reclaim it, so the memory moves rather than releasing. Raising it squeezes the tier below its floor into a refuse-all gate and ingestion falls back to disk. Hence: leave it unset, size the container.
  • Bounding Peak Memory with Concurrencymax_concurrent_queries defaults to 4× the CPU entitlement's cores and target_partitions to the cores; neither consults the memory limit. With the entitlement at twice the pod's CPU request, a requests.cpu: 4 pod admits 32 concurrent queries however little memory it has. This is the better first lever for peak memory — it lowers the peak, where lowering memory_limit only shrinks each query's share of it.
  • Check the Startup Budget Warnings First — the partition is computed before any data loads and warns when it cannot fit, at warn level. An unfittable configuration is detectable in the first seconds of a run rather than after days of load, so it belongs ahead of load testing. Four warnings tabulated with meaning and action. (The source comment on the first records a ~122 GiB host OOM-killed at SF-1000 whose only trace was that line.)

Also added: Reading a Memory Graph (a flat high plateau is caches at their ceilings and allocator retention, not a leak), Validating a Memory Configuration (the four properties that make a load test predictive — data volume, peak concurrency, duration to steady state, predicate diversity — plus shadow/canary), and Client-Side Resiliency (retry in-cluster before falling back; instance loss is independent of memory tuning).

Accuracy fixes found on the way

  • cayenne_segment_cache_mb was documented as a flat 256 default. It is auto-derived — 256 MB is the floor, and it reaches 1 GB above a 32 GiB host.
  • The cache-sizing example counted accelerator caches once per deployment. They are per dataset; the example now uses four and says what a fifth costs.
  • Two statements attributed the 70% base to "Cayenne acceleration is active". It is specifically the in-memory CDC tier being reachable — Cayenne bulk-only keeps the 90% base less the full reservation. Both now match the source, and agree with performance-tuning.md, which already had it right.

Supporting pages

The levers are surfaced where the problem is actually hit: performance-tuning gets a which-knob-for-which-intent table, deployment/kubernetes gets a Memory sizing section beside the CPU one, troubleshooting gets an OOM-despite-a-configured-limit entry.

Verification

cd website && npm run build passes — the config throws on broken links and anchors, so the build gates every cross-reference added here.

Constants checked against the shipped source: the 90/70/50 percentages, the 20% compaction carve, the tier's 1/5 ceiling, 1/32 floor and 1/4 float, the two cache clamps, the 128 MiB coalesce buffer, QUERIES_PER_CORE = 4, and target_partitions = cores.

Reviewer notes

Two things worth a maintainer's eye:

  1. The docs now state plainly that a small-data deployment can still legitimately need multiple gigabytes. That follows from the per-dataset floors and I believe it is correct, but it is the first place the docs say it outright.
  2. The startup warnings are documented by their leading phrases so they are greppable. Log strings are not a stable API — if someone rewords one, these tables go stale silently. Happy to point at the metrics instead if that is preferred.

Trunk website/docs/ only, so this lands as Next and flows into the next release; no versioned snapshots touched.

The memory guide gave sizing rules as multiples of dataset size and a
minimum RAM figure, with no account of the fixed cost those multiples sit
on top of. That reads as "memory scales with data", which makes a
proportionally downscaled staging environment look like a valid model of
production. It is not one, and the docs said nothing that would have
warned anyone off it.

Reframes the guide around `Total = Baseline + Working Set`. The baseline
is a function of DATASET COUNT, not data volume: each Cayenne table holds
a segment cache (1/128 of memory, clamped to 256 MB-1 GB), a CDC table
adds a PK keyset cache (1/32, clamped 256 MiB-8 GiB) and a coalesce
buffer. The clamps are the point — the per-dataset cost stops shrinking
once the environment is small, so it is a rounding error at production
volumes and the dominant term below them. A worked table shows the same
ten-dataset Spicepod at ~76% baseline in 4 GiB and ~9% in 32 GiB; the
existing multiples are relabelled as working-set figures.

Four things from the runtime that the docs did not carry, all of which
change what an operator should actually do:

`How the Runtime Partitions Memory` — the 70/20/10 partition (pools /
in-memory CDC tier / off-pool headroom), the compaction carve out of the
query pool, and the 50% floor. Makes the baseline/working-set split
concrete rather than conceptual, and gives the reservation clawback a
place to be explained.

`Tuning the Memory Limit Safely` — the limit is bounded on BOTH sides
when CDC is active, and both directions are counterintuitive. Lowering it
need not reduce resident memory: the tier is sized from what is left
after the pools and floats up to a quarter of memory to reclaim it, so
the memory moves rather than releasing. Raising it squeezes the tier
below its floor into a refuse-all gate and ingestion falls back to disk.
Hence the guidance to leave it unset and size the container.

`Bounding Peak Memory with Concurrency` — `max_concurrent_queries`
defaults to 4x the CPU entitlement's cores and `target_partitions` to the
cores; neither consults the memory limit. With the entitlement at twice
the pod's CPU request, a `requests.cpu: 4` pod admits 32 concurrent
queries however little memory it has. This is the better first lever for
peak memory: it lowers the peak, where lowering `memory_limit` only
shrinks each query's share of it.

`Check the Startup Budget Warnings First` — the partition is computed
before any data loads and warns when it cannot fit, at `warn` level. An
unfittable configuration is therefore detectable in the first seconds of
a run rather than after days of load, which is the cheapest sizing check
available and belongs ahead of load testing. Four warnings tabulated with
meaning and action. (The comment on the first records a ~122 GiB host
OOM-killed at SF-1000 whose only trace was that line.)

Also `Reading a Memory Graph` (a flat high plateau is caches at their
ceilings and allocator retention, not a leak — what matters is whether
and where it plateaus), `Validating a Memory Configuration` (the four
properties that make a load test predictive: data volume, peak
concurrency, duration to steady state, predicate diversity; shadow/canary
as the alternative), and `Client-Side Resiliency` (retry in-cluster
before falling back; instance loss is independent of memory tuning).

Accuracy fixes found on the way:

- `cayenne_segment_cache_mb` was documented as a flat `256` default. It
  is auto-derived — 256 MB is the floor and it reaches 1 GB above a
  32 GiB host.
- The cache-sizing example counted accelerator caches once per
  deployment. They are per dataset; the example now uses four and says
  what a fifth costs.
- Two statements attributed the 70% base to "Cayenne acceleration is
  active". It is specifically the in-memory CDC tier being reachable —
  Cayenne bulk-only keeps the 90% base less the full reservation. Both
  now match the source, and agree with performance-tuning.md, which
  already had it right.

Supporting pages pick up the levers where the problem is actually hit:
performance-tuning gets the which-knob-for-which-intent table,
deployment/kubernetes gets a Memory sizing section beside the CPU one,
troubleshooting gets an OOM-despite-a-configured-limit entry.

Every constant is checked against the shipped source: the 90/70/50
percentages, the 20% compaction carve, the tier's 1/5 ceiling and 1/32
floor and 1/4 float, the two cache clamps, the 128 MiB coalesce buffer,
`QUERIES_PER_CORE = 4`, and `target_partitions = cores`.
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

✅ Pull with Spice Passed

Passing checks:

  • ✅ Title meets minimum length requirement (10 characters)
  • ✅ Has at least one of the required labels: area/blog, area/docs, area/cookbook, dependencies
  • ✅ No banned labels detected
  • ✅ Has at least one assignee: lukekim

@github-actions

Copy link
Copy Markdown
Contributor

🚀 deployed to https://cd1df99b.spiceai-org-website.pages.dev

@lukekim lukekim self-assigned this Aug 11, 2026
Comment thread website/docs/deployment/kubernetes/index.md Outdated
Comment thread website/docs/reference/memory.md Outdated
Comment thread website/docs/reference/memory.md
Comment thread website/docs/reference/memory.md
Comment thread website/docs/reference/memory.md
Comment thread website/docs/reference/performance-tuning.md Outdated
Comment thread website/docs/reference/memory.md
…y cost, task history

Review feedback from @bjchambers and @sgrebnov.

The baseline was described as "present at idle", which is wrong in a way
that matters: most of it is bounded buffers — the SQLite metastore's page
cache, gRPC/HTTP serialization buffers, the accelerator caches — that
reach their ceilings only as traffic drives them there. It is the
CONVERGED footprint, not the startup footprint, and a freshly started
runtime under-reports it. Corrected here, on the Kubernetes page, and in
the cache-sizing example.

The working set was described as scaling with dataset size alone. It also
scales with the data each query touches — `SELECT 1` against `SELECT *`
against a multi-way join with a sort — and that per-query cost is then
multiplied by concurrency. Both terms therefore understate themselves
early, which is now stated where the model is introduced.

Task history added to the baseline table (@sgrebnov). It is an in-memory
accelerated table, so its footprint is task rate x `retention_period`
(8h default) rather than a fixed allocation, and `captured_plan` /
`captured_output` inflate every record. Recommends shortening retention
over disabling it — it backs `runtime.task_history`, the primary tool for
diagnosing exactly what this page is about.

The cache-sizing example gains a warning that it is a planning floor, not
a prediction: it sums only what can be named in advance and so
under-estimates actual usage. It omits traffic-driven buffer growth, task
history, allocator retention, and transient peaks, and the query memory
limit in it is a ceiling rather than a reservation. Not to be used as the
container limit.

`max_concurrent_queries` now states the trade-off in both places it
appears: it reduces peak MEMORY, and raises peak QUERY TIME, because
excess queries wait for admission and that wait counts toward end-to-end
duration. Says to measure total duration, not execution time.

Circuit breakers added to client resiliency: retries handle isolated
failures and make sustained ones worse, so after N consecutive failures
stop traffic for a cooldown and probe before restoring — noting this
usually belongs at the load balancer or mesh, where the decision is
shared across callers rather than relearned by each.

The "a plateau is not a leak" claim now appeals to the allocators'
own documentation rather than asserting it: jemalloc's page retention and
decay schedule, glibc's `M_TRIM_THRESHOLD`, and the cgroup v2 docs for
why RSS is nonetheless what `memory.max` charges. All three added to
External References.
…tory is time-bounded

Two precision fixes on the review changes. The Baseline bullet still said
the baseline is a function of dataset count alone, which the refinement
directly below it now contradicts — task history scales with task rate
and the buffers with traffic served. It now names both drivers.

The task history row read "Unbounded by size", which is literally true
(there is no byte cap) but invites the reading that it grows without
limit. It is bounded by time; the row now says so.
@github-actions

Copy link
Copy Markdown
Contributor

🚀 deployed to https://56174974.spiceai-org-website.pages.dev

@github-actions

Copy link
Copy Markdown
Contributor

🚀 deployed to https://7148aeb8.spiceai-org-website.pages.dev

@lukekim
lukekim merged commit 1783f1a into trunk Aug 11, 2026
6 checks passed
@lukekim
lukekim deleted the docs/memory-sizing-and-tuning-guidance branch August 11, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants