Home / Stories / Right-sizing
Story · Resources Live

Resource requests sized from what the workload actually used.

Somebody set the Mimir querier to 5.6 GiB of memory a long time ago, "to be safe". Eight replicas later, the cluster reserves 45 GiB for a component that has never used more than 80 MiB. This is how the assistant notices, decides what the right number is, and refuses to let a model's guess go below what was measured.

Starts from30 days of per-container CPU and memory usage in Prometheus or Mimir, per tenant.
Looks atUsage vs requests, OOM kills and restarts, how spiky the workload is, how its neighbours peak, the manifest or HelmRelease values in git.
You getA draft MR that changes only the resource values, with average, p95 and peak usage and the sizing rule in the description.
01 · Read the usage

Once a night, every container, 30 days back.

The metrics scan runs daily. For each metrics tenant it queries CPU and memory working set, the configured requests, OOM kills and restarts for every container in every namespace, over a 30-day window.

Nothing is collected by the assistant itself. It reads the Prometheus or Mimir data you already have, read-only.

02 · Find the outliers

Only the containers worth a look go further.

A pre-filter picks the interesting ones, always measured against the request:

  • average use under 30% of the request (reserving too much),
  • average, p95 or peak over 85% (about to hit the ceiling),
  • any OOM kill or more than 3 restarts in the last day,
  • no request at all, but real usage observed.

mimir-querier requests 5.6 GiB and uses about 64 MiB: roughly 1% of what it reserves.

03 · Look at the shape

Average, p95 and peak, across all replicas.

An average hides bursts, so the sizing never rests on it alone. For each container the assistant keeps the average, the 95th percentile and the peak working set, taking the highest value across all 8 replicas that ran in the window.

Here the three numbers sit close together: 64 MiB average, 76 MiB p95, 80 MiB peak.

04 · Classify

Flat or spiky? The answer changes the rule.

Volatility is peak divided by p95. At 1.25 or below the workload is flat: there are no real spikes, so a big gap between request and limit is just reserved memory nobody uses.

Above 1.25 it is spiky, and a second number decides how much headroom to reserve: namespace concurrency, whether the pods around it tend to peak at the same time or take turns.

The querier: 80 / 76 = 1.06, flat.

05 · Draft the change

The model proposes values, inside written rules.

The measured numbers, the classification and the current manifest go to the language model together with the sizing rules for each case. It returns a small structured proposal: which values to change, and why.

It is allowed to say "keep as proposed". Tuning for the sake of tuning is not the goal.

06 · Clamp it

Code, not the model, has the last word on the numbers.

Models sometimes ignore their own rules, so the hard limits are enforced again in code after the model answers:

  • memory request never below the observed peak,
  • memory limit never below the observed peak, never above five times it,
  • no more than a 5× gap between request and limit,
  • requests never above limits,
  • CPU limits removed: CPU is throttled, not killed, so a CPU limit only adds latency.

Here the model suggested a 64 MiB request. That is below the 80 MiB peak, so it was raised.

07 · Write the smallest diff

Only the resource values move.

The change is written back into the file in place, so comments, key order and every other value stay exactly as they were. Values are rounded up to standard steps (64, 96, 128 MiB…), never down.

For a HelmRelease the edit goes into spec.values, where the chart takes it from, not into rendered output.

08 · The proposal

The evidence travels with the change.

Every resource-touching MR carries the measurements it was sized from: classification, volatility, namespace concurrency, average, p95 and peak, how many replicas were observed, and which safety clamps fired. You can check the arithmetic without opening Grafana.

metrics scan · tenant platform · last 30d
# working set, per container
avg by (namespace, pod, container)
  (container_memory_working_set_bytes{container!=""})
# CPU, per container
rate(container_cpu_usage_seconds_total{container!=""}[5m])
# what each container asked for
kube_pod_container_resource_requests{resource="memory"}
# OOM kills that survive the restart
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}
→ 1,284 containers · 61 namespaces · 30 days
pre-filter · average memory use as % of request
checkout-web
62%
orders-api
48%
search-indexer
91%
mimir-querier
1.1%
request 5.6Gi × 8 replicas = 44.8Gi reserved actual use ≈ 0.6Gi
monitoring/mimir-querier · avg 1.1% of request · below the 30% threshold
mimir-querier · working set · 30 days · 8 replicas (max)
0 64Mi 128Mi peak 80Mi avg 64Mi p95 76Mi
working setp95peakday 1 → day 30
no OOM kills · 0 restarts · 8 replicas observed · request 5.6Gi
classification
volatility  = peak / p95 = 80 / 76 = 1.06   # ≤ 1.25 → flat
concurrency = 14.2%                # namespace pods rarely peak together
Flat (≤ 1.25)Keep request and limit tight: request near p95, limit a little above peak.
Spiky, spikes alignedNeighbours peak together: reserve for the peak, generous limit.
Spiky, spikes staggeredSafe to reserve typical use; the limit absorbs the burst.
Spiky, in betweenA blend of the two.
mimir-querier → flat · concurrency not needed for this case
model output · tune resources
{
  "requests": { "memory": "64Mi" },
  "limits":   { "memory": "128Mi", "cpu": "2800m" },
  "rationale": "Flat workload (1.06). Request to typical use,
                limit about 1.5x peak. Existing CPU limit kept."
}
Close, but two values break the rules. The next step catches both.
deterministic clamps · after the model
  • memory request 64Mi < observed peak 80Mi raised → 96Mi
  • memory limit 128Mi ≥ peak 80Mi kept
  • limit ≤ peak × 5 (400Mi) ok
  • limit ≤ request × 5 (480Mi) ok
  • CPU limit 2800m present removed
  • requests ≤ limits in the final file ok
final: request 96Mi · limit 128Mi · no CPU limit · 2 clamps fired
infrastructure/monitoring/helmrelease_mimir.yaml+2 −3
  values:
    querier:
      replicas: 8          # sized for query fan-out, see runbook
      resources:
        requests:
          cpu: 500m
-         memory: 5.6Gi
+         memory: 96Mi
        limits:
-         cpu: 2800m
-         memory: 5.6Gi
+         memory: 128Mi
comments, key order and the CPU request untouched
Draft · Right-size mimir querier based on 30-day usage
classification flat peak/p95 1.06 namespace concurrency 14.2% avg 64Mi · p95 76Mi · peak 80Mi 8 replicas over 30d clamps: request→peak, CPU limit removed
category:resourcessource:metricsrisk:mediumxrev::LGTM
↓ the full merge request is below

The merge request you get.

Illustrative numbers, real shape: the evidence block and the clamp notes are what every resource-touching MR carries today.

Draft

Right-size mimir querier based on 30-day usage

from gitops-ai/rightsize-mimir-querier-94a2 into main · 1 file · +2 −3
category:resourcessource:metricsrisk:mediumxrev::LGTM
OverviewEvidenceChangesAfter merge
Summary

The querier component of the mimir HelmRelease in monitoring requests 5.6 GiB of memory per replica and has used at most 80 MiB in the last 30 days, across all 8 replicas. This MR brings the request to 96 MiB and the limit to 128 MiB, and removes the CPU limit.

Why these numbers

The workload is flat (peak / p95 = 1.06), so request and limit are kept close: the request covers the observed peak, the limit leaves about 1.5× peak of headroom. Values are rounded up to the nearest standard step.

What the safety clamps changed
  • The drafted memory request (64 MiB) was below the observed peak and was raised to 96 MiB.
  • The CPU limit was removed. CPU is throttled rather than killed, so a limit adds latency under burst without protecting anything.
Released capacity

About 44 GiB of reserved memory across 8 replicas, which the scheduler can now give to other pods.

SignalValueSource
Classificationflatpeak / p95 ≤ 1.25
Volatility (peak / p95)1.06working set, 30d
Namespace concurrency14.2%joint vs summed peaks in monitoring
Memory avg / p95 / peak64Mi / 76Mi / 80Mimax across replicas
Replicas observed830-day window
OOM kills / restarts (24h)0 / 0kube-state-metrics
Current request / limit5.6Gi / 5.6Gi, CPU limit 2800mHelmRelease values
Applied request / limit96Mi / 128Mi, no CPU limitafter clamps
   values:
     querier:
       replicas: 8          # sized for query fan-out, see runbook
       resources:
         requests:
           cpu: 500m
-          memory: 5.6Gi
+          memory: 96Mi
         limits:
-          cpu: 2800m
-          memory: 5.6Gi
+          memory: 128Mi
After merge, expect
  • Flux upgrades the mimir HelmRelease; querier pods roll one at a time.
  • Querier working set stays under 96 MiB (container_memory_working_set_bytes, container querier).
  • No OOMKilled terminations on the querier over the next week.
  • Query latency unchanged on the Mimir reads dashboard.
Rollback

Revert this MR. The next nightly scan re-measures either way, and a workload that starts hitting its limit is picked up by the "over 85%" rule.

R
independent reviewer second opinion, no pipeline context

Checked the cited peak against the tenant's 30-day data and the chart's values path for querier.resources. Request covers peak, limit is within bounds. LGTM.

What keeps it honest.

The model proposes a number. Measured data and fixed rules decide whether it ships.

Never below the peakMemory requests and limits are floored at the highest working set seen across all replicas, after the model has answered.
No evidence, no cutA memory limit is only cut when a real peak or p95 series backs it. Average-only data raises floors; it never drives a cut.
Idle windows are not proofBackup sidecars, CronJobs and similar burst-shaped workloads that were only ever seen idle are not squeezed to their idle size.
Operator-managed pods handled gentlyPods run by an operator CRD keep their CPU limit and cannot be cut far below the original request without CPU evidence.
Round up, never downEvery value snaps up to a standard step, so formatting can never quietly under-provision.
Valid by constructionThe final file is checked for requests above limits before the MR is opened.

Where this stands.

Live
  • Nightly 30-day usage scan per metrics tenant
  • Flat / spiky classification with namespace concurrency
  • Deterministic clamps and step rounding
  • The same metric-grounded tuning applied to any resource change another agent proposes
  • First-time requests for workloads that have none
In development
  • Guard for burst-shaped workloads sampled only while idle (enabled, being measured)
  • Resolving the owning workload through owner references, so an autoscaler is never taken for the target
Roadmap
  • Request sizing MRs for autoscaled workloads whose utilisation target has no request to measure against (an issue today)
← All storiesBack to the story index