Kubernetes Cost Visibility in 2026: OpenCost, VPA InPlace, and the Right-Sizing Math That Actually Saves Money
Sources
- OpenCost v1.121.3 release (Sep 16, 2026) — GPU public pricing, IBM Cloud Usage Reports, local disk pricing
- OpenCost v1.121.2 release (Sep 11, 2026) — Public Pricing 1.0.0 module
- OpenCost Open Usage and Allocation Specification — max(request, usage) workload cost formula
- OpenCost committed public pricing data — modules/pricing/public (USD nodes.jsonl, 198,903 entries)
- OpenCost Helm chart 2.5.31 — values.yaml (exporter apiPort 9003, MCP server default-on port 8081)
- OpenCost API documentation — /allocation, /assets, /cloudCost endpoints
- Kubernetes docs — In-place resize of container resources (KEP-1287, GA in v1.35)
- VPA 1.7.x features — InPlaceOrRecreate (GA in VPA 1.6), InPlace mode (alpha), CPUStartupBoost (alpha)
- VPA 1.7.1 release (Jul 26, 2026) — recommender history-metric flag fix, checkpoint regression fix
- VPA recommender defaults — target percentile 0.9, safety margin 0.15, OOM bump-up ratio 1.2 (source)
- Goldilocks v4.16.2 release (Sep 15, 2026) — controller /metrics endpoint, InPlace regression tests
- Goldilocks source — VpaUpdateModeKey label goldilocks.fairwinds.com/vpa-update-mode, allowed modes
- Cloudflare — Saving another 100TB of RAM with math (and Rust), Sep 18 2026
- IBM newsroom — IBM acquires Kubecost (Sep 17, 2024)
- kubectl-cost — CLI for Kubernetes cost, OpenCost-compatible
- OpenCost AI Inference Cost Tracking — vLLM token metrics, cost bases allocation vs usage
September 19, 2026. Cloudflare bought itself back more than 100TB of RAM this week without adding a single server — by shrinking hash-ring data structures and fixing struct layout in Rust. It is the most visible entry yet in a genre of engineering that platform teams know well: capacity work that shows up as a real, defensible line in the budget. The Kubernetes version of the same discipline is less glamorous and more common: your pods are requesting resources they never use, and every one of those requests is money that the scheduler counts as spent.
Here is the uncomfortable mechanism: in Kubernetes, a resource request is not a suggestion — it is a reservation. The scheduler subtracts requests from node allocatable when deciding where a pod can fit, and your cost model bills you for max(request, usage). A pod that requests 2 CPU and averages 0.4 CPU doesn’t “cost a little more than it should” — it burns capacity as if it were fully loaded, and forces the cluster to scale out to hold the fiction. Multiply by a few hundred over-provisioned workloads and you get the silent 30–50% waste that every FinOps survey keeps re-finding.
The tooling to see and fix this has quietly matured to the point where the excuses are gone. OpenCost (the CNCF incubating project Kubecost originally open-sourced before IBM bought the company in 2024) shipped its Public Pricing 1.0.0 data module in v1.121.2 (September 11) — nearly 200,000 committed price entries across AWS, Azure, GCP, IBM, Oracle, and Alibaba — and followed with v1.121.3 (September 16) adding GPU public pricing for GCP and an IBM Cloud Usage Reports integration. VPA 1.7.x can now apply recommendations without evicting your pods on Kubernetes 1.33+ via in-place resize, and Goldilocks v4.16.2 (September 15) exposes a /metrics endpoint and regression coverage for InPlace mode. This guide puts the three layers together — visibility (OpenCost), recommendation (VPA/Goldilocks), and actuation (in-place resize) — with the actual math you should run before you claim a saving, because the difference between a request and a reservation is exactly where the money is.
The One Formula That Explains Kubernetes Cost
Everything in Kubernetes cost allocation reduces to a single decision written down in the OpenCost specification: workload cost = max(request, usage) × price. The spec is explicit that this “effectively assigns costs that have been directly reserved or allocated by kube-scheduler.” Usage only counts when it exceeds the request — burst above request is real capacity consumption too — but under-requested, over-consumed pods are billed for what they actually take.
The two faces of the same formula — and where the waste lives:
allocation cost = max(request, usage) x price <-- reconciles to the bill usage cost = usage x price <-- what the pod really consumed request >> usage ──▶ request-overhang waste (the classic right-sizing target) usage >> request ──▶ noisy-neighbor / throttle risk (the under-provisioning trap) ┌────────────────────────────────────────────────────┐ │ pod requests 2 CPU / 8 GiB │ │ pod uses 0.4 CPU / 2.1 GiB │ │ │ │ ████░░░░░░░░░░░░░░ request (reserved capacity) │ │ ██░░░░░░░░░░░░░░░░ usage │ │ ▲▲▲▲▲▲▲▲▲▲▲▲▲ the scheduler bills this whole │ │ bar regardless of utilization │ └────────────────────────────────────────────────────┘
Run the numbers on a real, current price. OpenCost’s committed public pricing data lists an AWS m6i.2xlarge (8 vCPU / 64 GiB) in us-east-1 at $0.384/hour on-demand (v1.121.3 data file). That works out to roughly $0.048 per vCPU-hour and $0.006 per GiB-hour on that instance family. Now the waste becomes concrete: a pod requesting 2 CPU / 8 GiB that peaks at 0.4 CPU / 2.1 GiB is allocated $0.144/hour of capacity while consuming $0.032/hour of resources. The $0.112/hour difference is pure request-overhang — $81.91/month for that one pod, $40,953/month across a fleet of 500 lookalikes. The bill reconciles to the request, not the usage, and that is the whole game.
Two consequences before we touch any tooling:
- Cost visibility is a data-pipeline problem, not a dashboard problem. You need per-container requests (kube-state-metrics), per-container usage (cAdvisor), and per-node prices (OpenCost’s public pricing files or your actual negotiated rates). If any of the three is missing, your numbers are fiction — and the request side of the triangle is the one teams skip, then wonder why the dashboard disagrees with the invoice.
- Idle cost is not waste until you prove it. OpenCost separates
__idle__allocation — node capacity nobody requested — from workload allocation. Idle exists to absorb scheduling slack and node failures; driving it to zero makes the cluster fragile. The target is workload overhang (requests >> usage), not a 100% packed fleet.
Layer 1 — Visibility: OpenCost Without the Guesswork
OpenCost is the CNCF incubating cost-allocation engine behind Kubecost, separated out as a spec plus a Go implementation. The install story changed recently and is now Helm-only — the standalone manifests are gone:
helm repo add opencost https://opencost.github.io/opencost-helm-chart
helm repo update
# Chart 2.5.31 ships OpenCost 1.121.x. The UI and MCP server are on by default.
helm upgrade --install opencost opencost/opencost \
--namespace opencost --create-namespace \
--set opencost.prometheus.internal.enabled=true \
--set opencost.prometheus.internal.serviceName=prometheus-server \
--set opencost.prometheus.internal.namespaceName=prometheus-system
kubectl -n opencost port-forward service/opencost 9090:80
# UI: http://localhost:9090
# API: http://localhost:9003 (exporter apiPort)
The pricing layer is what used to be OpenCost’s weak point — for years it leaned on live provider pricing APIs that rate-limited, changed shape without notice, or required keys for anything useful. The Public Pricing 1.0.0 release (merged August 10, shipped in v1.121.2) changed the architecture: pricing files are now committed to the repository under modules/pricing/public/ with per-provider subdirectories, and a GitHub Action tags and versions the module on every push to that tree. The v1.121.3 follow-up added GCP GPU pricing, AWS marketplace fees, and split basic pricing by provisioning model. What that means operationally:
- Costs are reproducible. The same OpenCost version always computes the same prices. Your cost reports stop drifting week over week because a provider pricing API hiccuped.
- No API keys for on-demand pricing. The chart’s
cloudProviderApiKeyvalue (for the GCP pricing API) is now only needed for evaluation scenarios — on-demand rates are in the committed data. - Versioned diffs. When AWS repricing lands, it shows up as a git diff between OpenCost versions. You can finally audit why last month’s number moved, and pin your deployment until you choose to reprice.
Once the exporter is running against your Prometheus, the allocation API is a single curl. The default query the OpenCost UI runs is:
curl -G http://localhost:9003/allocation \
-d window=7d \
-d aggregate=namespace \
-d resolution=1m
The parameters that actually matter, from the API docs:
| Parameter | What it does | When you use it |
|---|---|---|
window |
Time range: 30m, 12h, 7d, month, or explicit UTC timestamps |
Always. Use a window ≥7d for right-sizing — shorter windows chase noise and miss weekly peaks |
aggregate |
Grouping: namespace, controller, pod, container, label, comma-combined |
Chargeback by namespace first; container-level before editing any manifest |
shareIdle=true |
Distributes node idle cost across the pods that could have used it | Team chargeback — without it, idle hides in a shared bucket nobody owns |
idleByNode=true |
Keeps idle attributed per-node instead of shared | Cluster-autoscaler tuning — finding which node pools are over-provisioned |
accumulate=day |
Sums the window into daily totals (also hour, week) |
Trend lines and month-over-month deltas |
For terminal-first teams, kubectl-cost wraps the same APIs as a kubectl plugin (kubectl krew install cost) — and per its own README, “most of kubectl cost works” against OpenCost’s API, with kubectl cost namespace, pod, controller, and node views. The MCP server (on by default since chart 2.5.31, port 8081) even exposes cost queries to AI agents through a standardized interface — useful for an internal platform portal that lets service owners ask “what did my team spend last week” in plain language.
One deployment note that will bite anyone running sharded Prometheus: point PROMETHEUS_SERVER_ENDPOINT at a global query endpoint (Thanos Query, Cortex, or Mimir), not a single Prometheus pod — the OpenCost README warns that a single-pod endpoint produces incomplete, intermittent export results. The chart has dedicated opencost.prometheus.thanos and .amp blocks for exactly this.
Layer 2 — Recommendation: What VPA Actually Computes
The Vertical Pod Autoscaler’s recommender is the de-facto engine behind most right-sizing tooling — Goldilocks is a namespace-label veneer over VPA-in-recommendation-only mode. Knowing what it actually does with your usage histograms separates real savings from cargo-culting. From the 1.7.1 recommender source defaults:
| Knob (recommender flag) | Default | What it controls |
|---|---|---|
--target-cpu-percentile / --target-memory-percentile |
0.9 | The recommended request targets the P90 of the usage histogram — not the average, not the peak |
--recommendation-margin-fraction |
0.15 | Then adds a 15% safety margin on top of the P90 target |
--pod-recommendation-min-cpu-millicores / -memory-mb |
25m / 250MB | Floor values so tiny containers don’t get starved by rounding |
| Memory aggregation | 24h interval × 8 days | Memory is aggregated in 24-hour blocks kept for 8 days — the default window over which the P90 is taken |
| Histogram decay half-life (CPU & memory) | 24h | Old usage decays with a 24-hour half-life — a spike from three days ago counts, much less |
| OOM response | ×1.2, min +100Mi | After an OOMKill, memory recommendation bumps 20% and at least 100Mi — OOMs move the floor, not just the target |
Read that as a policy: “set requests to P90 of 8 days of usage, plus 15%”. That’s materially more conservative than “average × 1.5” folklore, and it self-documents — the recommendation you see in the VPA object is exactly what those parameters produce. For latency-critical or bursty services, the percentile is the knob to argue about: a P90 target means one bad hour in ten lands above the request, where it either throttles (CPU limits) or gets the node’s shared nothing (no limits). If you have CPU limits set and care about p99s, either raise the percentile or delete the limits — the middle ground is where surprise throttling lives.
Continue the worked example with those defaults. P90 usage of 0.35 CPU / 2.0 GiB, plus the 15% margin, gives a recommended request of 0.4025 CPU / 2.3 GiB — an allocation cost of $0.0331/hour against the original $0.1440/hour. That is a 77% cost reduction per pod, and here is the part most teams miss: it converts directly into node-count reduction. One hundred of these pods at 2 CPU each demand 200 schedulable cores — 25 m6i.2xlarge nodes, $7,008/month. The same hundred pods at VPA’s 0.4025 CPU fit in 6 nodes, $1,682/month. Right-sizing is not a percentage-point optimization; it is the difference between a 25-node fleet and a 6-node fleet, minus whatever headroom you deliberately keep.
Right-sizing chain — from usage histogram to node bill:
cAdvisor metrics VPA recommender scheduler / cluster-autoscaler
───────────────── ────────────────── ─────────────────────────────
container_cpu_usage_ P90 of 8d histogram ──▶ request 0.4025 CPU
seconds_total + 15% safety margin (was 2.0 CPU)
container_memory_
working_set_bytes OOM seen? bump x1.2 100 pods: 40.25 CPU total
(per container, (min +100Mi) vs 200 CPU before
24h x 8d windows)
┌─────────────────────────┐
│ │ 8 CPU/node (m6i.2xl) │
│ │ 6 nodes now │
▼ │ 25 nodes before │
OpenCost allocation API │ $1,682/mo vs $7,008/mo │
max(request,usage) x price └─────────────────────────┘
proves the before/after
The classic operational trap: VPA and HPA cannot both scale the same resource metric — the known-limitations doc is blunt that VPA-on-CPU plus HPA-on-CPU double-controls the same signal. The supported combination is VPA on memory with HPA on CPU (or HPA on custom/external metrics). Also remember VPA 1.7.x is not compatible with pod-level resources stanzas yet (AEP-7571 is in flight) — if you adopted pod-level resource managers, keep VPA off those workloads until that lands.
Layer 3 — Actuation: In-Place Resize Changes the Risk Model
Here is why right-sizing historically stalled in every org that tried it: applying a new request used to mean evicting and recreating the pod. VPA’s Recreate update mode is a controlled disruption loop, and for stateful or latency-sensitive workloads that trade was correctly rejected. Kubernetes’ in-place resize (KEP-1287) — beta in 1.33, GA since 1.35 — changes the actuation economics: resource requests can now change while the container runs, no restart, subject to each container’s resizePolicy (NotRequired by default, or RestartContainer for runtimes/apps that can’t adapt).
VPA rides this in 1.7.x with three actuation modes, and the differences matter operationally:
| updateMode | Since | Behavior | Disruption risk |
|---|---|---|---|
Off |
always | Recommendations computed and written to the VPA object; nothing applied | None — the honest starting point |
Initial |
always | Applies recommendations to new pods only, at creation, via the admission webhook | None to running pods; new pods start right-sized |
Recreate |
always | Evicts pods whose requests drift from the recommendation so controllers recreate them | Full pod restarts, budgeted via VPA’s eviction rules |
InPlaceOrRecreate |
VPA 1.6 [GA] | Attempts in-place resize first; falls back to recreation if infeasible/deferred >5 min, stuck >1h, QoS class would change, or memory-limit downscale hits a PreferNoRestart policy |
Small — container runtime performs the resize; fallback still evicts |
InPlace |
VPA 1.7 [alpha] | Eviction-free: only in-place updates, defers and retries when the node can’t take the resize; never evicts | None by design — but resizes may be deferred indefinitely on packed nodes |
The engineering detail worth internalizing: a resize succeeds at the node only if capacity is actually available, and kubelet reports back through the pod’s resize status — ResizeDeferred, ResizeInProgress, ResizeInfeasible, ResizeError. VPA’s updater in InPlace mode treats those as retry signals, not failures, and caches infeasible attempts so it only retries when the recommendation improves. Watch the updater’s metrics (vpa_updater_in_place_updated_pods_total, vpa_updater_failed_in_place_update_attempts_total) to confirm actuation is actually happening — a rightsizing program whose resizes all defer is just Off with extra steps.
The newest toy in 1.7 is CPUStartupBoost (alpha): the admission controller temporarily multiplies CPU at pod startup — startupBoost.cpu.type: Factor, factor: 3, durationSeconds: 10 — then scales back down in-place once the pod is Ready. It exists because right-sized JVMs and other startup-heavy runtimes otherwise take a slow-train to first request; the boost fixes the cold-start tax that P90 recommendations impose. Alpha, gated behind --feature-gates=CPUStartupBoost=true on both admission controller and updater, K8s 1.33+. Interesting, not yet load-bearing.
The Control Loop: Putting It Together With Goldilocks
Goldilocks is the glue that makes VPA recommendations consumable by teams rather than by a platform engineer with kubectl. v4.16.2 (September 15) added a controller /metrics endpoint (previously it was dashboard-or-nothing) and regression coverage for InPlace update modes — the maintainers are visibly tracking VPA’s new modes. The mechanics, from the source: Goldilocks watches namespaces for the label goldilocks.fairwinds.com/enabled=true, then creates a VPA per workload controller (Deployment, StatefulSet, DaemonSet) in recommendation-only mode, defaulting updateMode: Off — the safe read-only posture. Teams opt into more via the label/annotation goldilocks.fairwinds.com/vpa-update-mode, which in 4.16.2 accepts off, initial, recreate, inplaceorrecreate, and inplace (case-insensitive; the deprecated auto alias maps to Recreate).
# 1. Install Goldilocks (chart 11.1.1 / app v4.16.2) with the VPA recommender subchart.
# vpa.enabled=false by default: BYO VPA or let the chart install recommender-only.
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm upgrade --install goldilocks fairwinds-stable/goldilocks \
--namespace goldilocks --create-namespace \
--set vpa.enabled=true \
--set vpa.updater.enabled=false \
--set vpa.exporter.enabled=false
# 2. Enable recommendations for a team namespace (VPA objects created in Off mode)
kubectl label namespace payments goldilocks.fairwinds.com/enabled=true
# 3. Read what the recommender says (target / lowerBound / upperBound per container)
kubectl get vpa -n payments -o yaml | grep -A4 recommendation
# 4. Pick a mode per workload when you trust the numbers — alpha, opt in deliberately:
kubectl -n payments patch deployment checkout-api --type=merge \
-p '{"metadata":{"annotations":{"goldilocks.fairwinds.com/vpa-update-mode":"inplaceorrecreate"}}}'
# 5. Prove the change with OpenCost, before and after (share idle for chargeback honesty)
curl -G http://localhost:9003/allocation \
-d window=7d -d aggregate=namespace -d shareIdle=true \
| jq '.data[].allocations["payments"].totalCost'
# 6. Watch actuation, not intentions — the updater metrics tell you if resizes land:
# vpa_updater_in_place_updated_pods_total (going up = working)
# vpa_updater_failed_in_place_update_attempts_total (going up = investigate node capacity)
One warning from Goldilocks’ own installation docs that survives every version bump: the full VPA install includes the updater and admission webhook — the components that change pod specs and evict pods — and “an admission webhook can introduce unexpected results in a cluster if not planned for properly.” Goldilocks only needs the recommender. If you install VPA via the Goldilocks chart’s subchart with vpa.enabled=true, you get recommender-only by default (vpa.updater.enabled=false); if you install VPA yourself with ./hack/vpa-up.sh, you get the whole apparatus including the webhook mutating every matching pod. Know which one you installed.
The Sane Rollout Plan (and Who Should Skip This)
Everything above compresses to a sequence that respects the blast radius:
- Week 1 — measure, change nothing. OpenCost with
shareIdle=true, a 7-day window, aggregation by namespace then controller. Publish the numbers to teams before touching anything. The goal is a baseline nobody argues with — it reconciles to the invoice. - Week 2 — recommendations as read-only artifacts. Goldilocks on the namespaces, VPA in Off mode, recommendations visible to teams. The conversation to have: per-container, target vs. current requests, and which services have burst profiles that P90 underserves. No auto-anything yet.
- Week 3+ — actuate on the boring tier first. Stateless, replicated ≥2, owned by teams that opted in. Start with
Initialmode (new pods only — zero risk to running workloads), thenInPlaceOrRecreateon K8s 1.33+ clusters. Stateful sets, single-replica controllers, and anything with a startup-probe personality stay onOfforInitialuntil you have per-container resize policies reviewed (RestartContainerfor JVMs that can’t re-read their heap). - Continuously — close the loop with the same API. The after-baseline uses the identical OpenCost query. Report deltas in dollars and node counts, not utilization percentages — utilization is an input; the bill is the output.
Decision matrix — which actuation mode earns trust, per workload class:
workload class first move why
───────────────────────── ───────────────── ─────────────────────────────
stateless, replicas >= 2 InPlaceOrRecreate resizes land, fallback exists
bursty / p99-sensitive Initial only P90 target underserves tails;
fix percentile first
stateful / single replica Off + manual eviction cost > waste; review
per-release instead
JVM / fixed-heap runtimes resizePolicy: in-place memory downscale is
RestartContainer wasted on a runtime that
cannot act on it
pod-level resources (1.37 skip VPA VPA 1.7 incompatible (AEP-7571)
PodResourceManagers users) until support lands
Who should skip this article’s advice: if you are running a single node with a handful of dev workloads, the visibility stack costs more attention than it saves — read your invoice instead. If your cluster autoscaler scales to zero nightly and your workloads are entirely spot/reclaimed (e.g. Karpenter consolidation aggressively bin-packs), request right-sizing yields less because consolidation already attacks the same waste — though even there, OpenCost’s allocation view is how you prove Karpenter is doing its job. And if you have no baseline, do not start with VPA in any applying mode — recommendations without a measured baseline are just different guesses.
The Skeptic’s Appendix: Where This Stack Still Falls Short
- Requests don’t reconcile to the invoice on their own. OpenCost’s numbers are only as honest as your Prometheus retention and your pricing inputs. Committed-use discounts, reserved instances, savings plans, and enterprise discount programs are invisible to public on-demand pricing files — v1.121.x helps with on-demand reproducibility, but chargeback against real spend still needs your negotiated rates in
opencost.customPricingor the cloud-integration path. - The P90-plus-15% policy is a default, not a law. It is right for the median stateless service. It is wrong for anything with a startup spike (see CPUStartupBoost), a monthly batch peak, or hard p99 SLOs. Tune the percentile per class of service or don’t bother arguing about the recommendations.
- Goldilocks automates recommendation creation, not judgment. It will happily recommend right-sizing a workload whose memory profile is bimodal because two teams share a namespace. The namespace label is an administrative boundary, not a technical one — review per-controller before actuation.
- OpenCost UI + Kubecost divergence is real and growing. Kubecost the product (now IBM’s) and OpenCost the project share lineage and API shapes, but Kubecost’s agent, saved reports, and forecasting are product features on top — and the
kubecost/cost-modelrepo now redirects toopencost/opencost. If you started on Kubecost free tier years ago, that is not “OpenCost with a logo” — decide which side of the fork your platform lives on before building dashboards on either. - GPU and AI-inference costing is arriving, but young. OpenCost 1.121.3 just added GCP GPU public pricing, and the new inference cost tracking (vLLM token metrics → $/Mtok under allocation vs. usage bases) is promising and exactly where platform budgets are heading — but treat both as 1.0 features: verify against your bill before quoting them to finance.
The Cloudflare lesson generalizes beyond proxies: capacity engineering wins are measured wins — they start with an instrument that says exactly how much is wasted, and they end with a number that reconciles to the bill. On Kubernetes, that instrument is OpenCost, the recommendation engine is VPA, the actuation is in-place resize, and the math is one formula: max(request, usage) × price. Everything else is plumbing. Deploy the plumbing, trust the formula, and make the scheduler’s opinion of your pods match reality.