Last year somebody set cpu: 1 on a Deployment. It felt safe. Today the process uses about a third of that core.
The scheduler doesn't care. It holds the whole core on the node for every replica: at 3 a.m., all weekend, through every quiet hour of the month. Two thirds of that core sits reserved, doing nothing.
For years the fix meant restarting the pod, so nobody bothered. That excuse is gone. In-place resize is on by default from Kubernetes 1.33 and generally available in 1.35. You lower the CPU request on a running container, the process keeps its connections, and the node gets the reservation back.
Attune is a Kubernetes operator that does this for a fleet. It reads Prometheus, shows the estimate, and does not touch a pod until you say so. A bad size is put back. First you install it. Then you look. Then you cut.
Install
Attune needs Kubernetes 1.32 or newer. On 1.32, in-place resize still needs the InPlacePodVerticalScaling feature gate. Webhooks are on by default and need cert-manager. If you don't run it, add --set webhooks.enabled=false to the Helm command.
helm install attune oci://ghcr.io/attune-io/charts/attune \ --namespace attune-system --create-namespace kubectl krew install attune Point a policy at the services you suspect. Recommend only writes numbers.
apiVersion: attune.io/v1alpha1 kind: AttunePolicy metadata: name: api-services namespace: production spec: targetRef: kind: Deployment selector: matchLabels: tier: api metricsSource: prometheus: address: http://prometheus-server.monitoring:80 cpu: percentile: 95 overhead: "20" minAllowed: "1m" memory: percentile: 99 overhead: "30" minAllowed: "4Mi" updateStrategy: type: Recommend These are the defaults, written out so the knobs are visible. The policy covers every tier: api Deployment in production. CPU lands at the 95th percentile plus 20 percent headroom. That "20" is a percentage, not 20 millicores. Memory gets the 99th percentile plus 30 percent.
kubectl apply -f policy.yaml If Prometheus already has the history, the number shows up on the first pass. A brand-new series needs 48 samples at the default 5 minute step, about 4 hours. For a faster first look, set minimumDataPoints: 12 under metricsSource. That is about 1 hour at the same step. Leave queryStep at the default so it stays near your scrape interval. After the numbers look right, drop the field and use 48.
Look
The number you came for is in these two commands:
kubectl attune recommendations -n production kubectl attune savings -n production recommendations puts the live CPU request next to the recommended one. savings prices that gap at built-in list rates, about $0.031 per core-hour. The monthly figure is an estimate.
Here is the Deployment from the opening. Usage sits around a third of the request at the busy end, so this illustration recommends 400m: that level plus 20 percent. Memory is already at the recommendation, so the saving is the CPU column. These rows are not a cluster we measured. F means the request is far above the recommendation.
$ kubectl attune recommendations -n production NAMESPACE POLICY WORKLOAD CONTAINER CPU REQ CPU REC MEM REQ MEM REC GRADE CONFIDENCE / STATUS production api-services api app 1 400m 512Mi 512Mi F 90.0% $ kubectl attune savings -n production NAMESPACE NAME CPU SAVED MEMORY SAVED % SAVED EST. MONTHLY production api-services 600m - 60% $13.58 TOTAL 600m 0B 60% $13.58 600m is the request Attune would hand back on that container. $13.58 is 0.6 cores times $0.031 times 730 hours. With ten replicas that is 6 cores, about $136 a month. savings still prints the one container, so multiply by your replica count.
If you want that cut, change the type. If you don't, leave Recommend. Nothing has been resized.
To see that the policy is only looking:
kubectl get attunepolicies -n production NAME TYPE WORKLOADS RECS RESIZED READY AGE api-services Recommend 1 1 0 True 5m RECS counts workloads with a recommendation. RESIZED stays 0 while the type is Recommend.
Cut one pod, or a slice
When you leave Recommend, Attune changes CPU and memory. CPU can go down. Memory can go up, and it does not go down unless you set memory.allowDecrease: true. That default keeps a tight number from OOM-killing a JVM that sized its heap at startup.
OneShot resizes one pod that still needs it, waits out a 1 hour cooldown, then resizes the next. Ten replicas of one Deployment take about ten hours. Switch the type back to Recommend to stop. Pods already resized keep the new size.
Canary resizes a slice and then waits:
updateStrategy: type: Canary canary: percentage: 10 observationPeriod: 5m The block needs both fields, or the API server rejects it. autoPromote defaults to false, so the rest of the fleet stays put until you set that to true or change the type to Auto, which resizes the rest.
When the new size is wrong
If a resized container is OOMKilled within 5 minutes (safetyObservationPeriod), Attune puts the old request back. With Prometheus, CPU throttling above half of the periods does the same. A peak next Monday does not trigger a revert. A smaller size fits on the node. A later increase, or that revert, might not. The default is to wait and retry. The pod is not evicted.
There is one trap: an HPA that scales on CPU utilization. Utilization is usage divided by the request. Shrink the request and the same pod suddenly looks hotter. The HPA adds replicas and the savings walk out the door. Attune rewrites the HPA target only when the HPA carries attune.io/auto-tune: "true". Without that annotation, set minAllowed high enough that the request can't fall further than you planned.
Make the next deploy keep it
An in-place resize changes the running pod. It does not change the Deployment in Git, so Argo CD and Flux do not call it drift. The next rollout still starts from the Git request.
If Git owns the Deployment, leave the policy on Recommend and commit the number:
updateStrategy: type: Recommend export: configMap: true kubectl attune diff -n production -o yaml That patch is what you commit. It carries the new requests for each Deployment. The ConfigMap is there for tools that read the numbers. Attune can also open a drift-report pull request. The GitOps guide covers dry run, and what not to turn on while Git owns the Deployment.
Other guides
- A cluster autoscaler can drop a node once the requests are smaller. Attune does not do that drop. Bin packing.
- The process only needs a big CPU while it starts. A startup boost raises CPU for that window and then drops it. If
maxAllowedis already the steady request, the boost cannot rise. - A resize during the lunch rush is a bad time. A schedule blocks the resize and still computes the number. Auto mode.
- An error rate or a latency query matters more than an OOM. SLO guardrails put the size back when that query breaches.
- The estimate can go to Slack or Teams once a week. Weekly report.
How are you setting CPU and memory today? If you try a Recommend policy, tell me in the comments how it goes.