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 44.8 GiB for a component that has never used more than 80 MiB. This is a real case. A gap this large is unusual, but it makes each step easy to see. Here is how the assistant notices this and decides the right number. It never lets the guess of an AI model (LLM) go below what it measured.

Starts from30 days of CPU and memory usage for each container. The data comes from Prometheus or Mimir (metrics stores), for each tenant.
Looks atUsage against requests, OOM kills (a container stopped for lack of memory) and restarts. How spiky the workload is, and when its neighbours peak. The manifest or HelmRelease values in git.
You getA draft merge request (MR) that changes only the resource values. The description shows the average, p95 and peak usage, and the sizing rule.
01 · Read the usage

Once a night, every container, 30 days back.

The metrics scan runs daily. For each metrics tenant, it asks for the CPU and memory working set (the memory a container really uses). It also asks for the configured requests, OOM kills and restarts. It does this for every container in every namespace, over a 30-day window.

The assistant does not collect any data 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. It always compares usage with the request (the amount the container asks the cluster to reserve):

  • average use under 30% of the request (it reserves too much),
  • average, p95 or peak over 85% (it is 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 (p95) and the peak working set. It takes the highest value across all 8 replicas that ran in the window.

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

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. It has no real spikes. A big gap between request and limit is then just reserved memory that nobody uses.

Above 1.25 it is spiky. A second number then decides how much headroom to reserve. This number is namespace concurrency. It shows if the pods around it 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 assistant sends the measured numbers, the classification and the current manifest to the AI model. It also sends the sizing rules for each case. The AI model returns a small structured proposal. The proposal lists which values to change, and why.

The AI model may answer "keep as proposed". The goal is not to tune for the sake of tuning.

06 · Clamp it

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

AI models sometimes ignore their own rules. So the code enforces the hard limits again after the AI 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 AI model suggested a 64 MiB request. That is below the 80 MiB peak, so the code raised it.

07 · Write the smallest diff

Only the resource values move.

The assistant writes the change back into the file in place. Comments, key order and every other value stay exactly as they were. It rounds values up to standard steps (64, 96, 128 MiB…), never down.

For a HelmRelease (a Flux object that installs a Helm chart), the edit goes into spec.values. The chart reads its values from there. The edit does not go into rendered output.

08 · The proposal

The evidence travels with the change.

Every MR that touches resources carries the measurements it used for sizing. These are the classification, volatility, namespace concurrency, average, p95 and peak. They also include how many replicas the assistant 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.

The numbers come from an example run, but the shape is real. Every MR that touches resources carries this evidence block and these clamp notes 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. In the last 30 days it used at most 80 MiB, across all 8 replicas. This MR brings the request to 96 MiB and the limit to 128 MiB. It also removes the CPU limit.

Why these numbers

The workload is flat (peak / p95 = 1.06), so request and limit stay close together. The request covers the observed peak. The limit leaves about 1.5× peak of headroom. The assistant rounds values up to the nearest standard step.

What the safety clamps changed
  • The drafted memory request (64 MiB) was below the observed peak. The code raised it to 96 MiB.
  • The code removed the CPU limit. CPU is throttled, not killed. A limit adds latency under burst and protects nothing.
Released capacity

About 44 GiB of reserved memory across 8 replicas. The scheduler can now give it 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 restart 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 measures again either way. If a workload starts to hit its limit, the "over 85%" rule finds it.

R
independent reviewer second opinion, no pipeline context

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

What keeps it honest.

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

Never below the peakAfter the AI model answers, the code never allows a memory request or limit below the highest working set across all replicas.
No evidence, no cutThe assistant cuts a memory limit only when a real peak or p95 series backs the cut. Average-only data raises floors. It never drives a cut.
Idle windows are not proofSome workloads work in bursts, such as backup sidecars and CronJobs. If the assistant only ever saw them idle, it does not shrink them to their idle size.
Operator-managed pods handled gentlyPods run by an operator (a controller that manages them through a custom resource) keep their CPU limit. Without CPU evidence, the assistant cannot cut them far below the original request.
Round up, never downEvery value snaps up to a standard step. Formatting can never quietly under-provision.
Valid by constructionThe assistant checks the final file for requests above limits before it opens the MR.

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 that the assistant sampled only while idle (enabled, being measured)
  • Finding the owning workload through owner references, so the assistant never takes an autoscaler 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