Skip to content
Go back

OpenCost on k3s: What Your Pods Cost

By KingPin 13 min read
OpenCost on k3s: What Your Pods Cost
Contents

“It’s Free, I Already Own It” Is a Lie Your Cluster Tells You

Your home lab does cost money. It just bills you in slow motion. The mini PCs cost real dollars, the wall outlet charges you every hour, and nobody sends an invoice, so nothing ever looks expensive. That is how you end up with 14 forgotten test deployments quietly holding half your RAM while Jellyfin, the one thing your family actually uses, gets blamed for the power bill.

The fix is OpenCost, the CNCF cost-allocation project. Out of the box it prices your pods with cloud list prices, which is meaningless on hardware you own. Give it custom on-prem rates derived from your electricity price and what you paid for the boxes, and it tells you what every namespace costs per month. This post shows the whole path on k3s: Prometheus, the Helm install, the rate math (with a script that actually runs), and how to read the answer.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/devops/opencost-k3s-home-lab-cost

Versions below were verified in October 2026: OpenCost app 1.121.3, Helm chart 2.5.32. Check the chart repo before you copy anything, because chart values move.

Why Bother Putting a Price on Pods You Already Own

Three reasons, in order of how much they will annoy you.

First, idle nodes. A node you bought sits there drawing watts whether you schedule anything or not. Cost per workload only makes sense once you also know how much of the cluster nobody is using. Paying for a forklift to idle in the garage is a decision. It should be one you made on purpose.

Second, requests. Kubernetes reserves what a pod asks for, not what it uses. That 2 GB request on a service that idles at 90 MB blocks 2 GB from everything else. OpenCost prices the larger of the request and the usage, so over-asking shows up as dollars.

Third, the junk drawer. Every cluster collects test-nginx-3, a half-finished Gitea experiment, and a Grafana you installed “just to look”. They each request a little. Together they add up to more than the thing you care about. A number next to each namespace ends the argument.

What OpenCost Needs: Prometheus, and a Home

OpenCost does not collect metrics itself in the standard setup. It reads them from Prometheus, so Prometheus is a hard prerequisite. The OpenCost docs say so plainly: Prometheus handles the scraping and the data storage.

You have two paths.

Path A: you already run kube-prometheus-stack. Good. You already have kube-state-metrics and node-exporter, which OpenCost needs. Point OpenCost at your existing Prometheus service and move on. If you do not have one yet, the cAdvisor and Prometheus post covers the metrics side, and the stack installs with a single Helm command.

Path B: nothing yet. The OpenCost on-prem docs give a standalone Prometheus install in the prometheus-system namespace with an extra scrape config. That works, but if you want dashboards and alerts anyway, install kube-prometheus-stack and skip the dedicated one. Running two Prometheus instances on a three-node mini PC cluster is how you earn a 2 AM page about memory.

Check what your Prometheus service is actually called before you write any values:

Terminal window
kubectl -n monitoring get svc | grep prometheus

With a release named kps and the kube-prometheus-stack chart, the service is typically kps-kube-prometheus-stack-prometheus on port 9090. Your release name decides this, so read it from the cluster, not from this post.

Installing OpenCost with Helm on k3s

The chart lives at https://opencost.github.io/opencost-helm-chart, the chart name is opencost, and k3s needs nothing special. Here is the values file. The keys come from the chart’s own values.yaml.

values.yaml
opencost:
exporter:
defaultClusterId: homelab
prometheus:
internal:
enabled: true
serviceName: kps-kube-prometheus-stack-prometheus
namespaceName: monitoring
port: 9090
metrics:
serviceMonitor:
enabled: true
additionalLabels:
release: kps
customPricing:
enabled: true
createConfigmap: true
provider: custom
costModel:
description: Home lab rates from power bill and hardware cost
CPU: "0.00065393"
RAM: "0.00007006"
storage: "0.00000057"

Three notes on that file.

The prometheus.internal block is the in-cluster Prometheus address. The chart defaults assume a service called prometheus-server in prometheus-system on port 80, which is the standalone install from Path B. If you use kube-prometheus-stack, you override all three. For a Prometheus outside the cluster, the chart also has opencost.prometheus.external.enabled and external.url.

The serviceMonitor block makes the Prometheus Operator scrape OpenCost’s own metrics. The release: kps label has to match what your Prometheus selects, or the monitor is created and silently ignored. That is the classic gotcha with operator-based setups.

The customPricing block is where the money lives. We will fill in those three rates in a minute. Install:

Terminal window
helm install opencost --repo https://opencost.github.io/opencost-helm-chart opencost \
--namespace opencost --create-namespace -f values.yaml

The pod is up when kubectl -n opencost get pods shows it running. OpenCost needs a while to accumulate history, so give it a few hours before you trust any window longer than “today”.

The Rate Math: From Power Bill to Per-Core-Hour

The custom pricing fields are CPU, RAM, and storage, plus optional GPU, spotCPU, spotRAM, and the network egress fields. The shipped defaults in default.json are priced from GCP us-central1: CPU is 0.031611, RAM is 0.004237, storage is 0.00005479452. These are hourly rates. CPU is dollars per core-hour, RAM is dollars per GB-hour, storage is dollars per GB-hour. Those defaults price your mini PC like a cloud VM, about 48 times higher on CPU than our derived rate.

So where do your numbers come from? Two sources.

  1. Electricity. Measured average watts, divided by 1000, times your rate in dollars per kWh. The power math post covers the watts to kWh to bill conversion, and the Kill A Watt tour covers measuring each box. I use the same $0.13/kWh example rate as the power math post. It is an example. Use the rate on your bill.
  2. Hardware. Purchase price divided by the hours in your amortization period. Five years is 43,830 hours.

Add them and you have a cost per node-hour. Then you have to split that between CPU, RAM, and disk, and here I will be upfront: the split is a choice, not a law of physics. The script I use gives disk 10% of the hardware cost and splits the remaining compute cost 70/30 between CPU and RAM. Both are flags. Change them if you disagree, because nobody can prove those numbers wrong.

Here is the script run for a node in a three-node cluster: a $250 refurbished mini PC, five-year amortization, 18 W measured average, 8 cores, 32 GB RAM, 1000 GB disk.

Terminal window
python3 derive_rates.py --kwh-rate 0.13 --watts 18 --price 250 --years 5 \
--cores 8 --ram-gb 32 --disk-gb 1000

Real output:

Electricity: $0.0023/hour
Hardware: $0.0057/hour
Node total: $0.0080/hour ($5.88/month)
CPU: $0.00065393 per core-hour
RAM: $0.00007006 per GB-hour
Storage: $0.00000057 per GB-hour
{
"provider": "custom",
"description": "On-prem rates derived from power bill and hardware cost",
"CPU": "0.00065393",
"RAM": "0.00007006",
"storage": "0.00000057"
}

The core of the script is a handful of lines:

derive_rates.py
power_hr = watts / 1000 * kwh_rate
hw_hr = price / (years * 8766) # hours per year
disk_hr = hw_hr * disk_share # default 0.10
compute_hr = power_hr + hw_hr - disk_hr
CPU = compute_hr * cpu_weight / cores # default 0.70
RAM = compute_hr * (1 - cpu_weight) / ram_gb
storage = disk_hr / disk_gb

One node costs $5.88 a month all in. Three identical nodes cost $17.64. Pasting the JSON-shaped values into costModel is the whole configuration. For a mixed cluster, either sum cores, RAM, disk, watts, and price across all nodes into one run, or run it per node and average. OpenCost takes one set of rates for the whole cluster.

What do these rates mean in pod terms? One core reserved for a month costs about $0.48. One GB of RAM for a month costs about $0.05. That is the entire economics of your lab: a fully booked 24-core, 96 GB cluster is around $16 of compute a month. Not scary. Which is the point: the rates are small enough that the lesson is about proportion, not about saving money on your power bill this month.

Reading the Bill: Allocation by Namespace

Port-forward the deployment. The API listens on 9003 and the UI on 9090:

Terminal window
kubectl -n opencost port-forward deployment/opencost 9003 9090

Open http://localhost:9090 for the UI, which gives you cost by namespace, workload, and window. For scripting, hit the API:

Terminal window
curl -G http://localhost:9003/allocation/compute \
-d window=7d \
-d aggregate=namespace \
-d accumulate=true

window takes words like today and lastweek, durations like 30m or 7d, or RFC3339 date pairs. aggregate takes namespace, controller, pod, node, label:<name>, and a few more, and accepts comma-separated lists. Add includeIdle=true to see an __idle__ bucket, and shareIdle=true to spread that idle cost across the real workloads. Pipe the result through jq to sort by total cost.

If you prefer a terminal table, the kubectl cost plugin is still around. It started as the Kubecost CLI, lives in the kubecost/kubectl-cost repo (latest release v0.6.6, last pushed in September 2026), and the OpenCost docs list it as an integration. Install it with kubectl krew install cost, then point it at OpenCost, which uses a different service name and port than Kubecost:

Terminal window
kubectl cost --service-port 9003 --service-name opencost \
--kubecost-namespace opencost --allocation-path /allocation/compute \
namespace --historical --window 7d --show-cpu --show-memory --show-pv

Now do the arithmetic that makes this useful. Say Jellyfin requests 2 cores and 4 GB. At our rates that is 2 x $0.48 + 4 x $0.05, about $1.16 a month. Say each of your 14 forgotten test deployments requests 500m CPU and 512 MiB. Each costs about $0.26 a month, and all 14 together cost about $3.70. The pile of junk costs more than three Jellyfins. Those figures are hand calculations from the derived rates, not captured OpenCost output, but the mechanism is exactly what the allocation API reports.

You do not need to shout about $3.70. You do need to notice the ratio. Your own cluster will have a different ratio, and it will be the first time you have seen it.

Idle Cost, Requests, and Why Your Estimates Are Wrong in a Useful Way

OpenCost charges each container the greater of what it requested and what it used. That rule is in the OpenCost spec, and it is why this tool nags you about requests.

A namespace that requests 8 cores and uses 0.3 is billed for 8. The difference is wasted money in the spreadsheet sense, and wasted headroom in the 2 AM sense, because the scheduler thinks the node is full while the CPU graph is flat.

Idle cost is the other half. Nodes with unreserved capacity have a cost nobody owns. includeIdle=true surfaces it as __idle__. A big idle number on a home lab usually means you bought more than you scheduled, which is fine, since headroom is the point of a lab. A big idle number alongside pods that are Pending means your requests are inflated.

The cure for inflated requests is measurement, not guessing. The Goldilocks and VPA post shows how to get recommended requests from real usage. Run Goldilocks, shrink the fat requests, and watch the namespace cost drop in OpenCost the next day. That loop is the real payoff of this whole exercise.

Accuracy: What This Number Is and Is Not

OpenCost does not read your power meter. It multiplies reserved and used resources by the rates you typed in. The electricity cost lives inside those rates, so the result is only as good as your wattage average and your amortization guess.

That makes it an allocation tool. It answers “which namespace should carry more of the bill,” not “what will my utility company charge.” A node that idles at 18 W and spikes to 60 W under a transcode will break an average. Re-run the script when you swap hardware.

When It Is Worth Running vs a Spreadsheet

My opinion: a spreadsheet wins if you have one or two nodes and a dozen workloads. You can compute those costs in your head, as we just did for Jellyfin.

OpenCost wins when the cluster is big enough that you cannot remember what is on it. Three or more nodes, a dozen namespaces, and a habit of installing things “just to try”. At that point you want the data to arrive without you doing the counting.

It also costs resources. Prometheus plus OpenCost is not free on a small cluster, and running a cost tool that costs more than the thing it measures is a very Kubernetes way to spend a weekend. If you already run Prometheus, the marginal cost is small. If you would install Prometheus only for this, think twice.

Kubecost, the Commercial Sibling

Kubecost is the commercial product built on the OpenCost engine. IBM announced it had acquired Kubecost in September 2024, and the product is now sold as IBM Kubecost under the Apptio line. OpenCost itself remains a CNCF project, accepted in June 2022 and moved to Incubating in October 2024. For a home lab, OpenCost is the one you want, since it is the open piece and the on-prem pricing docs target it directly.

Common Questions

Does OpenCost work without Prometheus?

Not in the standard setup. The OpenCost docs list Prometheus as a prerequisite for scraping and storing metrics. The chart also has a collectorDataSource option that collects cluster data without a Prometheus dependency, but it is disabled by default, so plan on Prometheus for a typical home lab install.

How much RAM does OpenCost plus Prometheus use on a small k3s cluster?

Budget for Prometheus first, because it is the heavy part. Its memory grows with metric count and retention, while the OpenCost pod itself is a small Go service. I did not benchmark a fixed number here, so read kubectl top pods in the monitoring namespace after a day and size from that.

Does OpenCost include my electricity bill directly?

No. OpenCost never reads your utility bill or a power meter. You calculate hourly CPU, RAM, and storage rates that already include electricity and hardware amortization, then enter them as custom pricing. The reported dollars are an allocation of that modeled cost.

What is the difference between Kubecost and OpenCost?

OpenCost is the open source CNCF project for Kubernetes cost allocation. Kubecost is the commercial product built on that engine, now owned by IBM, with added features and paid support. For a home lab, OpenCost covers namespace and workload cost allocation without a license.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Migrating 10 Years of Bookmarks
Next Post
Train a Tiny GPT, Part 4: Inference

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts