OpenCost in depth: how GPU cost is allocated in Kubernetes
Contents
Notation: amounts in euros (N €), decimals with a point. European references (OVH is among the supported providers). The dollar sign is not used (on this site it is a formula delimiter).
What this article covers
Second article in the FinOps track (A2), and the first tool deep dive. The FinOps introduction established that OpenCost is the CNCF standard for cost allocation; here the box gets opened: how it allocates cost internally, where it gets its data from, how on-prem prices are configured in euros, what its API exposes, and where the traps are. Understanding the mechanics matters because it defines what any tool on top (Kubecost included) can and cannot do, and because a badly set base price invalidates the whole split. No recommendations; only the mechanics and the methodology.
What OpenCost is
OpenCost is a vendor-neutral, Apache 2.0 project, originally built by Kubecost and donated to the CNCF (incubating project). It is a cost allocation layer for Kubernetes: it reads resource usage from the cluster, joins it with a price, and splits the cost across Kubernetes dimensions down to the container (OpenCost · GitHub). It supports dynamic pricing via the billing APIs of AWS, Azure and GCP, and custom prices for on-prem clusters (OpenCost · configuration).
What it is not: it is not an optimiser, nor a governance system, nor a product unit economics tool. It is the piece that answers “how much does each namespace/pod/team cost?”, and leaves the rest to the layers above. That is why it is the foundation of the FinOps track: without correct allocation, there is no cost per token and no TCO model worth anything.
The cost model: node-level allocation
The key to OpenCost is that the model works at node level. It starts from the node’s resource capacity (CPU, RAM, GPU, storage) and its total price, and splits that price across the resources. When the provider does not give explicit per-resource prices, OpenCost uses the ratio of a set of base prices (marginal rates, customisable) and normalises them so that the sum of the components equals the node’s total price (OpenCost · on-prem).
This is what makes it work on-prem: you declare the node cost (depreciated capex + opex), and OpenCost splits it without needing a cloud invoice. The normalisation guarantees that no cost is “lost” or “invented”: what you pay for the node is exactly what gets split across its resources.
Where the data comes from: Prometheus
OpenCost instruments nothing of its own: it reads from Prometheus, which is a prerequisite of the installation (OpenCost · GitHub). The sources:
| Source | What it provides |
|---|---|
kube-state-metrics | state of K8s objects (pods, requests/limits) |
node-exporter | node resources and capacity |
cAdvisor | real CPU/memory usage per container |
dcgm-exporter (NVIDIA) | GPU usage, memory and power |
With those series, OpenCost automates the join between Kubernetes resource usage and the
provider price, reducing the custom PromQL you would otherwise write by hand (Grafana ·
manage costs).
The GPU signal comes from DCGM via the dcgm-exporter (part of the NVIDIA GPU Operator),
the same base as GPU observability. Without
Prometheus and without DCGM exporting, OpenCost does not see the GPU.
Architecture: the pieces
OpenCost is not a monolith; it is three responsibilities worth telling apart:
| Piece | Function |
|---|---|
| Cost model | resolves the node price and splits/normalises it across resources |
| Allocation API | serves the allocated cost per dimension (/allocation and endpoints) |
| Exporter | publishes cost metrics on /metrics for Prometheus/Grafana |
The data flow: Prometheus holds the usage series (from kube-state-metrics, cAdvisor, dcgm-exporter); the cost model joins them with the resolved price; the Allocation API serves them aggregated by whatever dimension you ask for; and the exporter returns them to Prometheus so that Grafana can plot them. It is a cycle: metrics come in from Prometheus and cost goes back to Prometheus enriched. That symmetry is what lets you reuse your observability stack without standing up a new database: OpenCost lives on top of Prometheus, not beside it.
An operational note: historical analysis depends on Prometheus retention. For cost reports spanning months, a long-term backend behind it (Thanos, Mimir, VictoriaMetrics) is advisable, or periodically exporting the allocations to a store of your own.
On-prem prices in euros: the piece that decides everything
On-prem, the most important figure, and the most neglected, is the node price. It is configured
per provider; the options are alibaba, aws, azure, gcp, oracle, ovh or default
(on-prem) (OpenCost · configuration).
For on-prem you use default, supplying base prices in a default.json (or by overriding the Helm
values.yaml); anything you do not override uses the chart’s value (OpenCost · on-prem).
To set the node price in euros, the calculation is the TCO model one: depreciated capex + opex. An example of an 8×H100 node:
| Component | Calculation | €/hour |
|---|---|---|
| Capex (node ~240,000 €, 36 months) | 240,000 ÷ 26,280 h | ~9.13 |
| Energy (5.6 kW × PUE 1.4 × 0.058 €/kWh, France) | ~0.455 €/h | ~0.46 |
| Maintenance, networking, operations | estimate | ~1.5 |
| Node total | ~11.1 €/h |
That ~11 €/h is the number you put into OpenCost, and everything is split from it. And here is the documented trap: with the default pricing configuration, OpenCost under-prices CPU and GPU on-prem (issue #3781). If you do not adjust the base prices to your real cost, the split will be systematically low and the cost per token you report will be unreal. Configuring the node price is not optional: it is the figure everything else hangs from.
Configuring the price: an example
The base price is given in the Helm values.yaml (or a default.json), in euros per hour and per
unit of resource, derived from the node’s total cost:
# values.yaml — on-prem prices in €/hour (8xH100 node, France)
opencost:
customPricing:
enabled: true
provider: custom
costModel:
description: "On-prem 8xH100 node"
CPU: "0.030" # €/CPU-hour
RAM: "0.004" # €/GB-hour
GPU: "1.30" # €/GPU-hour ← the figure that moves cost/token the most
storage: "0.0002" # €/GB-hour
OpenCost normalises those values so that, multiplied by the node capacity, they add up to the
total price you declared (~11 €/h in the example). The number to be most careful with is the
GPU one: at 1.30 €/GPU-hour, the 8 cards are ~10.4 €/h, the bulk of the node’s cost. A badly set
GPU base shifts the whole cost per token. Always review the resolved prices in
/allNodePricing after configuring, because the default under-pricing bug bites exactly here.
Allocation: by usage, not by presence
The behaviour that has changed most and that matters most for GPU: in recent versions, the cost allocated to a GPU is determined by its usage, not its presence. If a GPU costing 100 €/month is only half used, the allocated cost is ~50 € ([search]). This aligns allocation with reality, you pay for what you use, but it has a consequence: the other half does not disappear, it is idle that somebody keeps paying for. OpenCost exposes it, and that is where the optimisation lever comes from.
OpenCost allocates to any Kubernetes dimension: cluster, node, namespace, controller (deployment, statefulset and so on), service, pod and container, and by labels (team, product). Access is through the Allocation API and several endpoints that expose the mechanics:
| Endpoint | What it returns |
|---|---|
/allocation | cost allocated by the dimension you ask for |
/costDataModel | the resolved node price |
/allNodePricing | hourly prices per node |
/pricingSourceSummary | summary of the pricing source |
/metrics | cost metrics for Prometheus |
The /metrics endpoint is what turns OpenCost into a Prometheus exporter: once there, you can
write PromQL to calculate the cost and efficiency of any Kubernetes concept and build panels in
Grafana (OpenCost · exporter).
Allocation API parameters
/allocation is controlled by a handful of parameters worth knowing, because they define which
number you get:
| Parameter | What it controls | Example |
|---|---|---|
window | time range | 7d, today, specific dates |
aggregate | aggregation dimension | namespace, label:team, pod |
accumulate | sum the range or break it per interval | true (total) / false (series) |
idle | include idle cost or not | true / false / separate |
filter | filter by namespace, label and so on | namespace:"llm-prod" |
The idle parameter is the most revealing: with idle=separate, OpenCost returns the idle
cost as a separate row, which lets you see at a glance how much is being paid for unused
capacity. A query with aggregate=label:team and idle=separate over window=30d is, literally,
the monthly chargeback report with the waste highlighted.
GPU in depth: DCGM, usage, MIG and idle
The GPU is the expensive resource, so its allocation deserves detail:
- Usage signal:
dcgm-exportergivesDCGM_FI_DEV_GPU_UTIL(utilisation),DCGM_FI_DEV_FB_USED(memory), and power. OpenCost uses utilisation to allocate by usage. - MIG (Multi-Instance GPU): an A100 is split into up to 7 isolated instances, which allows cutting idle from 50 % to almost 0 % by sharing one card across small workloads ([search]; see sharing the GPU: time-slicing, MPS and MIG). The cost allocation of each MIG instance follows the same usage logic.
- Idle: the reliable detection pattern combines three pieces, DCGM metrics in Prometheus, an
alert that fires when
DCGM_FI_DEV_GPU_UTIL < 10for more than 15 minutes, and routing of that alert to the team that owns the namespace ([search]).
An attribution nuance worth knowing: MIG creates isolated instances that Kubernetes sees as distinct resources, so OpenCost allocates them cleanly, each one to its pod. Time-slicing, by contrast, shares the same physical GPU across several pods with no compute isolation: there, per-pod utilisation is harder to separate, and cost attribution becomes approximate (the GPU is split across the pods sharing it, according to their measured usage). The practical rule: if you need exact per-team chargeback on a shared GPU, MIG gives cleaner attribution than time-slicing, at the price of rigid fixed partitions. It is a trade-off between cost accuracy and flexibility that is better decided before, not after, building the split.
Efficiency: the number that triggers action
With cost allocated by usage, OpenCost lets you calculate efficiency: how much of the allocated cost corresponds to useful work against the total. A GPU at 30 % utilisation has an efficiency of 30 %: the remaining 70 % is idle cost that somebody pays without receiving work. That figure, per team and per namespace, is what turns a dashboard into a chargeback conversation: “your namespace has 5 GPUs allocated at 25 % efficiency; either you raise utilisation or you release three cards”. It connects with FinOps and multi-tenancy: allocation locates the waste; scheduling and co-residency recover it.
Worked example: the allocation report
On the ~11 €/h node (8×H100), three teams share the cluster for a month. What a
/allocation?aggregate=label:team&idle=separate&window=30d query returns:
| Team | GPU-hours | Avg. util. | Allocated cost (€) | Efficiency |
|---|---|---|---|---|
| A · chat-prod | 2,880 (4 GPU) | 78 % | ~3,744 | high |
| B · batch | 1,440 (2 GPU) | 55 % | ~1,872 | medium |
| C · experimentation | 1,440 (2 GPU) | 22 % | ~1,872 | low |
| idle (separate) | — | — | ~1,100 | — |
What the report reveals: the cost allocated to A, B and C comes out of their GPU-hours and the node price; but the idle row (~1,100 €/month) is capacity nobody used and everyone pays for in the pro rata. And the efficiency column points at C: 2 GPUs at 22 % is ~1,560 € a month of which only ~340 € is useful work. That is the actionable figure: C does not have a model problem, it has a utilisation problem. Without OpenCost, those ~1,100 € of idle and C’s inefficiency stay diluted in an aggregate invoice nobody questions. With it, they are a named row and a number in euros.
Useful PromQL queries
As a Prometheus exporter, OpenCost lets you build the panels by hand. Some reference queries (exact
metric names vary by version; check in your /metrics):
# Cost per node and hour (resolved price)
node_total_hourly_cost
# GPU cost allocated per namespace (€/hour)
sum by (namespace) (
container_gpu_allocation * on(node) group_left node_gpu_hourly_cost
)
# Average GPU utilisation per namespace (for efficiency)
avg by (namespace) (DCGM_FI_DEV_GPU_UTIL)
# Idle GPUs: utilisation < 10 % sustained
DCGM_FI_DEV_GPU_UTIL < 10
The last one, wrapped in an alert with for: 15m and routed to the namespace owner, is the
standard idle detection pattern. With these series in Grafana you have cost per team, efficiency
and waste on one dashboard, the “Inform report” of the FinOps phase in real time.
Dynamic cloud prices vs custom on-prem prices
The difference is worth understanding, because it changes how reliable the figure is:
- Cloud (aws/azure/gcp/oracle/ovh): OpenCost obtains the price dynamically from the provider’s billing API, so the allocated cost reflects the real rate (including discounts, spot and so on) without you declaring anything.
- On-prem (
default/custom): you declare the price, derived from your TCO model. The advantage is total control; the risk is that a badly calculated price skews everything. That is why the TCO model (depreciated capex + opex) is not an academic exercise: it is literally OpenCost’s input.
The ovh provider among those supported is relevant for a European platform: if part of the
workload goes to a European sovereign cloud, OpenCost can allocate its cost with dynamic pricing,
and mix it with on-prem in the same allocation view.
From pod cost to cost per token
OpenCost gets as far as “this vLLM pod cost X €/hour”. The cost per token, the metric that compares on-prem against cloud, needs one more piece: the gateway (LiteLLM) that counts tokens per request and per team, as covered in the FinOps introduction. The full chain is: OpenCost gives the pod cost by usage → the gateway gives the tokens per team → dividing them gives the cost per token per team. OpenCost is the hardware half of that equation; without it, you would know the tokens but not their real cost. That is why A2 (allocation) is the foundation the whole cost per token of the proposal hangs from.
Deployment: Helm + Prometheus
The typical deployment is via Helm, with Prometheus as a prerequisite (for scraping and series storage). Conceptual steps:
- Have Prometheus with
kube-state-metrics,node-exporter,cAdvisoranddcgm-exporter. - Install OpenCost with Helm, pointing it at your Prometheus.
- Configure the node price in euros (
default.json/values.yaml), the step most often neglected. - Expose
/metricsand build panels in Grafana with PromQL over the cost metrics.
As a Prometheus exporter, OpenCost lets you write PromQL for the cost and efficiency of any concept and create bespoke dashboards (OpenCost · exporter). It reuses infrastructure a GPU cluster already has for observability (DCGM, Prometheus, Grafana): OpenCost only adds the pricing and allocation layer.
OpenCost vs Kubecost (just the essentials)
OpenCost is the free base; Kubecost (IBM) is the commercial product built on it. Briefly, what the commercial one adds:
| Capability | OpenCost | Kubecost |
|---|---|---|
| Cost allocation (CPU/GPU/mem/PV) | ✓ | ✓ |
| GPU via DCGM | ✓ | ✓ (3.0) |
| Long historical retention | depends on your Prometheus | included |
| Automatic rightsizing | — | ✓ (Turbonomic) |
| Governance, alerts, enterprise RBAC | basic | ✓ |
| Commercial support | community | ✓ (IBM) |
For most on-prem clusters with a platform team, OpenCost covers allocation; Kubecost adds value when you want automated optimisation and enterprise features without operating it yourself. The detail of the comparison, and when paying is worth it, is article A3.
Implementation checklist
For OpenCost to give a defensible per-team cost, order matters:
| Step | Action | Verification |
|---|---|---|
| 1 | Prometheus with kube-state-metrics, node-exporter, cAdvisor | series present |
| 2 | NVIDIA GPU Operator + dcgm-exporter | DCGM_FI_DEV_GPU_UTIL in Prometheus |
| 3 | Install OpenCost (Helm) pointing at Prometheus | /allocation responds |
| 4 | Configure the node price in € (capex+opex) | /allNodePricing shows the real one |
| 5 | Label pods/namespaces by team and product | aggregate=label:team splits correctly |
| 6 | Grafana panels for cost, efficiency and idle | the waste is visible |
| 7 | Idle alert (util<10 for: 15m) to the owner | the alert reaches the team |
| 8 | (optional) long-retention backend | months of history available |
Step 4 is the one most often skipped and the one that most invalidates the result. If you only do one thing right, make it that one: without the correct node price in euros, steps 5 to 7 split a wrong cost with great precision.
Limits and traps (data-driven)
- Default under-pricing. With the default pricing config, OpenCost underestimates on-prem CPU and GPU (#3781). Adjust the base prices to your real cost or the whole split will come out low.
- It depends on Prometheus and DCGM. Without the right series (especially
dcgm-exporter), there is no GPU allocation. Input data quality rules. - Allocation by usage ≠ total cost. A GPU at 50 % charges half; the other half is idle that still costs. Do not confuse “allocated cost” with “cost paid”.
- Prometheus retention. Historical cost analysis depends on how much your Prometheus retains; for long series, plan a long-term backend.
- Allocation, not per-token measurement. OpenCost reaches the pod; cost per token needs the gateway (LiteLLM) above it, as covered in the FinOps introduction.
With OpenCost’s mechanics clear, the next article (A3) compares what Kubecost and the commercial alternatives add, to decide which FinOps stack to adopt. But the foundation, correct allocation with the node price properly set in euros, is this one.
Closing
OpenCost looks like “installing a cost tool” and is, in reality, declaring what your hardware is worth and letting it split itself. That simplicity is its strength, it lives on top of the Prometheus and DCGM you already have, with no new database, and also its trap: the split is only as good as the node price you declare, and the default value under-prices on-prem GPU. Properly configured in euros, OpenCost turns the cluster’s aggregate invoice into a split per team, per product and per GPU, separates used cost from idle, and exposes the efficiency that triggers chargeback conversations. It is the hardware half of the cost per token equation, the other half, the tokens, is supplied by the gateway, and without this half there is no TCO model or on-prem vs cloud comparison that holds up. For a sovereign architecture proposal, OpenCost is the piece that makes cost stop being an intuition and become a table in euros, reproducible and auditable, with the idle GPU named and identified. The Inform phase of GPU FinOps starts here, and everything else hangs on this number being right.
See also
- GPU chargeback and showback — from OpenCost’s cost allocation to the per-team split with budgets (Kueue) and token attribution (LiteLLM).
- GPU utilisation as a FinOps lever — the cost of idle and how occupancy moves the allocated cost.
- TCO of an on-premise GPU cluster: depreciation, energy and infrastructure — the CAPEX, the depreciation and the €/GPU-hour that OpenCost needs as a base price when the node is not cloud but your own on-premise hardware.
Sources
- OpenCost · GitHub (CNCF, Apache 2.0) — https://github.com/opencost/opencost
- OpenCost · configuración (proveedores: aws/azure/gcp/oracle/ovh/default) — https://opencost.io/docs/configuration/
- OpenCost · configuración on-prem (precios personalizados) — https://opencost.io/docs/configuration/on-prem/
- OpenCost · exportador de Prometheus — https://opencost.io/docs/integrations/opencost-exporter/
- OpenCost · issue #3781 (infra-precio CPU/GPU on-prem por defecto) — https://github.com/opencost/opencost/issues/3781
- NVIDIA · dcgm-exporter — https://github.com/NVIDIA/dcgm-exporter
- Grafana · manage costs (OpenCost + Kubernetes) — https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/