Self-Hosted Energy-Aware Workload Scheduling with Kubernetes and Kepler
Quick Summary (TL;DR) Kepler is a CNCF sandbox Prometheus exporter that uses eBPF plus hardware power models (Intel/AMD RAPL, GPU telemetry) to estimate per-pod energy in watts/joules. Deploy it as a DaemonSet, scrape the metrics with Prometheus, then feed them into a scheduler extender, descheduler, or policy engine to make energy-aware placement decisions. The payoff: lower power draw, better bin-packing, and a smaller carbon footprint — ideal for homelabs and edge clusters where every watt matters.
Introduction: The Need for Energy-Aware Kubernetes
Kubernetes is brilliant at scheduling for CPU, memory, and I/O — and almost blind to the one resource that actually costs you money every second of the day: electricity. A default scheduler will happily spread workloads across every node in the cluster, keeping idle machines powered on, spinning fans, and drawing baseline wattage for no reason.
In a cloud environment, that waste is abstracted behind a bill. In a homelab or edge deployment, it shows up on your electricity meter, in your rack’s thermals, and in the lifespan of your hardware. A rack of five nodes idling at 40 W each burns roughly 1.75 MWh per year doing nothing useful.
Energy-aware scheduling closes this gap. Instead of treating power as an invisible side effect, you measure it, expose it as a first-class metric, and schedule against it. The tool that makes this practical in Kubernetes is Kepler.
Understanding Kepler: Architecture and Metrics
Kepler (Kubernetes-based Efficient Power Level Exporter) is a CNCF sandbox project that estimates the energy consumption of individual containers. It does not require expensive power meters per node. Instead, it combines two signals:
- eBPF-based telemetry — tracking CPU cycles, cache misses, and I/O per cgroup to attribute resource usage to specific containers.
- Hardware power models — reading Intel/AMD RAPL (Running Average Power Limit) counters, NVIDIA GPU telemetry, and ACPI data to establish a total node power budget.
Kepler then proportionally attributes that node-level power to each pod based on its observed resource usage. The result is a per-container wattage estimate good enough for scheduling decisions.
flowchart LR
A[Hardware Sensors<br/>RAPL / GPU / ACPI] --> C[Kepler DaemonSet]
B[eBPF cgroup<br/>Counters] --> C
C --> D[Prometheus<br/>Metrics Endpoint :9102]
D --> E[Prometheus Server]
E --> F[Grafana Dashboards]
E --> G[Scheduler Extender /<br/>Descheduler / KubeGreen]
G --> H[Kubernetes API<br/>Scheduling Decisions]
Key metrics exposed on /metrics:
| Metric | Description |
|---|---|
kepler_container_joules_total | Cumulative energy per container (joules) |
kepler_container_package_joules_total | CPU package energy per container |
kepler_node_platform_joules_total | Total node platform energy |
kepler_container_cpu_watts | Instantaneous CPU power per container (watts) |
Because energy is cumulative, use rate() in PromQL to get current wattage: rate(kepler_container_joules_total[1m]).
Prerequisites and Installation
Before installing Kepler, confirm your environment meets these requirements:
- Linux kernel 4.18+ with BTF and eBPF enabled (
CONFIG_DEBUG_INFO_BTF=y). - Hardware power sensors — Intel RAPL (
intel_raplmodule), AMD RAPL, or NVIDIA GPUs with DCGM. - Privileged access — Kepler needs
CAP_SYS_ADMINand host mounts for/sysand/proc. - Prometheus already deployed (or plan to install it).
Check kernel support:
uname -r
ls /sys/class/powercap/ # RAPL domains
grep CONFIG_DEBUG_INFO_BTF /boot/config-$(uname -r)
Note: Cloud VMs frequently virtualize away RAPL access. Kepler will still run but fall back to less accurate estimation models. Bare metal gives the best fidelity.
Deploying Kepler in Your Cluster
The fastest path is the official Helm chart:
helm repo add kepler https://sustainable-computing-io.github.io/kepler-helm-chart
helm repo update
helm install kepler kepler/kepler \
--namespace kepler \
--create-namespace \
--set serviceMonitor.enabled=true
Verify the DaemonSet is running on every node:
kubectl get pods -n kepler -o wide
kubectl logs -n kepler daemonset/kepler --tail=20
You should see log lines confirming RAPL domains were detected and the exporter is listening on port 9102. Quick sanity check:
kubectl port-forward -n kepler svc/kepler 9102:9102
curl -s localhost:9102/metrics | grep kepler_container_joules_total | head
Collecting and Visualizing Power Metrics
Add a scrape job to your Prometheus config:
scrape_configs:
- job_name: kepler
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
regex: kepler
action: keep
metrics_path: /metrics
scrape_interval: 15s
Then build a Grafana panel. Useful queries:
# Total cluster power (watts)
sum(rate(kepler_node_platform_joules_total[1m]))
# Power per namespace
sum by (namespace) (rate(kepler_container_joules_total[1m]))
# Top 5 power-hungry pods
topk(5, sum by (pod_name) (rate(kepler_container_joules_total[1m])))
The official Kepler Grafana dashboard (ID 16455 on grafana.com) gives you a ready-made starting point with per-node and per-namespace breakdowns.
Integrating Energy Metrics with Scheduling
Kubernetes’ default scheduler scores nodes on resource fit, affinity, and spread — not power. To make energy a scheduling input, you have three practical architectures:
| Approach | Mechanism | Complexity | Best For |
|---|---|---|---|
| Scheduler Extender | HTTP webhook scores nodes using live Kepler data | High | Production clusters needing custom logic |
| Descheduler | Periodic rebalancing based on energy policies | Medium | Consolidating idle workloads |
| Policy Engine (KubeGreen) | CRD-driven scaling and placement rules | Low | Homelabs, quick wins |
The simplest effective pattern is a descheduler that evicts pods from underutilized nodes so they can be powered down or put to sleep.
sequenceDiagram
participant D as Descheduler
participant P as Prometheus (Kepler)
participant K as Kube API
D->>P: Query node power usage
P-->>D: kepler_node_platform_joules_total
D->>D: Identify node below threshold
D->>K: Evict pods from idle node
K-->>D: Pods rescheduled to busy nodes
D->>K: Cordon idle node
Implementing Energy-Aware Scheduling Policies
Here’s a concrete, working policy using the Kubernetes descheduler with energy-driven eviction. First, identify idle nodes via PromQL, then annotate them for cordoning:
# Find nodes drawing less than 15W average
curl -s "http://prometheus:9090/api/v1/query" \
--data-urlencode 'query=rate(kepler_node_platform_joules_total[5m]) < 15' \
| jq -r '.data.result[].metric.node'
A minimal descheduler policy that drains these nodes:
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: energy-aware
pluginConfig:
- name: LowNodeUtilization
args:
thresholds:
cpu: 20
memory: 20
pods: 20
targetThresholds:
cpu: 70
memory: 70
pods: 70
plugins:
balance:
enabled:
- LowNodeUtilization
For true power-aware scoring, write a Scheduler Framework plugin that reads Kepler metrics and returns a node score inversely proportional to current wattage. Pseudocode:
func (p *EnergyPlugin) Score(ctx, state, pod, nodeName) (int64, *Status) {
watts := p.prom.QueryWatts(nodeName) // from Kepler
maxWatts := p.prom.ClusterMaxWatts()
// Prefer nodes already drawing power (bin-packing)
score := int64(100 * (watts / maxWatts))
return score, nil
}
This bin-packing bias concentrates workloads on fewer nodes, letting the rest idle or sleep.
Case Study: Homelab and Edge Scenarios
Homelab (3-node mini-PC cluster): A user running three NUC-class nodes found that Kepler revealed one node idling at 11 W while two others sat at 28 W. After enabling a bin-packing descheduler policy, they consolidated workloads onto two nodes and scheduled the third to suspend overnight via a cron-triggered systemctl suspend. Measured savings: ~0.5 kWh/day, roughly 180 kWh/year.
Edge (retail kiosk fleet): An edge deployment of 40 in-store nodes used Kepler to detect that ML inference pods were spread evenly, preventing any node from entering low-power states. By pinning inference to dedicated nodes with GPU acceleration and consolidating management pods, they cut average per-node draw by 18% and reduced thermal throttling incidents.
The lesson in both cases: visibility comes first. You cannot optimize what you cannot measure, and Kepler makes the invisible visible.
Best Practices and Optimization Tips
- Scrape frequently but not aggressively. A 15s interval balances resolution against eBPF overhead.
- Use
rate()with a window matching your scrape interval to avoid noisy wattage readings. - Combine energy data with utilization. A node drawing 50 W at 90% utilization is efficient; one drawing 50 W at 5% is a consolidation target.
- Set node power budgets as annotations and let policy engines consume them.
- Test on staging first. Aggressive descheduling can cause churn and even increase power use through repeated pod restarts.
- Pair with node sleep/wake automation (Wake-on-LAN,
systemctl suspend) for the biggest wins in homelabs. - Watch for eBPF overhead on high-throughput nodes; monitor Kepler’s own CPU footprint.
Conclusion and Future Directions
Energy-aware scheduling is moving from novelty to necessity. As electricity costs rise and sustainability reporting becomes mandatory in more jurisdictions, understanding the watt-level cost of every workload will be as routine as tracking CPU and memory.
Kepler gives you the measurement layer today. The scheduling layer is maturing fast — expect tighter integration with the Kubernetes Scheduler Framework, standardized carbon-intensity signals (via projects like the Green Software Foundation’s Carbon Aware SDK), and native power-aware autoscaling in KEDA and HPA.
Start small: deploy Kepler, watch your dashboard for a week, and let the data tell you where your watts are going. The first consolidation policy you write will likely pay for the effort within a month.
FAQ
What is Kepler and how does it measure power consumption?
Kepler (Kubernetes-based Efficient Power Level Exporter) is a Prometheus exporter that uses eBPF to monitor container-level resource usage and combines it with hardware power models (e.g., RAPL, GPU telemetry) to estimate energy consumption in watts. It exposes metrics like kepler_container_joules_total per container, pod, and namespace.
Can I use Kepler on any Kubernetes cluster? Kepler requires Linux kernel 4.18+ with eBPF support and access to hardware power sensors (e.g., Intel RAPL, AMD RAPL, or NVIDIA GPU metrics). It works on most bare-metal and VM clusters, but cloud instances may lack direct sensor access, limiting accuracy.
How do I integrate Kepler metrics with Kubernetes scheduling? You can use the Kepler metrics with a custom scheduler extender or a policy engine like KubeGreen. Alternatively, you can deploy a descheduler with energy-aware policies or use the Kubernetes Scheduler Framework to create a plugin that scores nodes based on their current power usage.
What are the benefits of energy-aware scheduling? Energy-aware scheduling can reduce overall power consumption by consolidating workloads onto fewer nodes, avoiding over-provisioning, and shifting workloads to nodes with better power efficiency. This leads to lower energy costs and a smaller carbon footprint, especially in homelabs and edge clusters.
Is Kepler production-ready? Kepler is actively developed by the Cloud Native Computing Foundation (CNCF) as a sandbox project. While it is used in production by some organizations, it is still evolving. For critical workloads, test thoroughly and consider it as part of a broader observability and optimization strategy.
How can I get started with energy-aware scheduling in a homelab? Start by deploying Kepler to measure your current power usage. Then, experiment with simple policies like bin-packing or node affinity based on power metrics. Tools like KubeGreen or custom scripts using the Kubernetes API can help automate scheduling decisions.