---
title: "OpenCost in depth: how GPU cost is allocated in Kubernetes"
url: https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/
markdown: https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/index.md
lang: en
date: 2026-06-14
lastmod: 2026-09-13
author: "José Manuel Román"
description: "A data deep dive into OpenCost: the node-level cost model, where it gets its data from (Prometheus, DCGM), how on-prem prices are configured in euros, allocation by usage (not by presence), the Allocation API and its endpoints, GPU/MIG, idle detection and deployment with Helm. Including the default under-pricing trap. State of the art 2026, with sources."
tags: ["finops","opencost","kubernetes","gpu","dcgm","prometheus","cost-per-token","cncf","data-series"]
translations:
  es: https://blog.lo0.es/posts/opencost-cost-allocation-kubernetes/index.md
---

# OpenCost in depth: how GPU cost is allocated in Kubernetes


> 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](https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/index.md) 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](https://github.com/opencost/opencost)).
It supports dynamic pricing via the billing APIs of AWS, Azure and GCP, **and custom prices for
on-prem clusters** ([OpenCost · configuration](https://opencost.io/docs/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](https://opencost.io/docs/configuration/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.

> **Diagram:** OpenCost cost model: total node price split and normalised across CPU, GPU, RAM and disk, and then allocated by usage to each pod — [see the HTML version](https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/)

---

## 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](https://github.com/opencost/opencost)). 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](https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/)).
The **GPU** signal comes from DCGM via the `dcgm-exporter` (part of the NVIDIA GPU Operator),
the same base as [GPU observability](https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/index.md). 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](https://opencost.io/docs/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](https://opencost.io/docs/configuration/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](https://github.com/opencost/opencost/issues/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:

```yaml
# 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](https://opencost.io/docs/integrations/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-exporter` gives `DCGM_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](https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/index.md)).
  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 < 10` for 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.

> **Diagram:** GPU allocation by usage in OpenCost: used cost charged to the pod, idle cost flagged to the namespace owner, and MIG splitting one card across workloads — [see the HTML version](https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/)

---

## 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](https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/index.md):
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`):

```promql
# 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](https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/index.md).
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:

1. Have Prometheus with `kube-state-metrics`, `node-exporter`, `cAdvisor` and `dcgm-exporter`.
2. Install OpenCost with Helm, pointing it at your Prometheus.
3. Configure the **node price** in euros (`default.json` / `values.yaml`), the step most often
   neglected.
4. Expose `/metrics` and 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](https://opencost.io/docs/integrations/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)

1. **Default under-pricing.** With the default pricing config, OpenCost underestimates on-prem
   CPU and GPU ([#3781](https://github.com/opencost/opencost/issues/3781)). Adjust the base prices
   to your real cost or the whole split will come out low.
2. **It depends on Prometheus and DCGM.** Without the right series (especially `dcgm-exporter`),
   there is no GPU allocation. Input data quality rules.
3. **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".
4. **Prometheus retention.** Historical cost analysis depends on how much your Prometheus retains;
   for long series, plan a long-term backend.
5. **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](https://blog.lo0.es/en/posts/gpu-chargeback-showback-multi-tenancy/index.md) — from OpenCost's cost allocation to the per-team split with budgets (Kueue) and token attribution (LiteLLM).
- [GPU utilisation as a FinOps lever](https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/index.md) — the cost of idle and how occupancy moves the allocated cost.
- [TCO of an on-premise GPU cluster: depreciation, energy and infrastructure](https://blog.lo0.es/en/posts/full-tco-on-premise-gpu-cluster-capex-gpu-hour-break-even/index.md) — 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/

