How to scale pods based on Carbon Intensity?

audio-thumbnail
How to scale pods based on carbon intensity
0:00
/454.238095

Listen to me read this post here (not an AI-generated voice!) or subscribe to the feed in your podcast app.


Like every other spam e-mail of the service you've forgotten you signed up to, I want to start this article with:

Hi there, it's been a while!

And it certainly is. Or was. I don't know. English is not my mother language. But that's not important.

What's important is the article you're reading. The idea for it came to me quite organically, sort of like a duh moment. I held a talk recently at Heapcon 2025 in Belgrade, on the topic From Kubernetes to Low Carbonetes: Optimizing Infrastructure and Workloads for Sustainability. There I demoed briefly a setup where I scaled the application based on the Carbon intensity of the grid. Here is where the duh moment was - I could write a deeper dive into what I've done. And here we are.

One short trivia before we continue - if you are a keen follower of my writings, you might have noticed that the title of the talk sounds familiar. Yes, I've already written on this topic some time ago. In that article, as in my talk this October, I covered some basics, and practical steps on how you can reduce your Carbon footprint within your Kubernetes clusters.

From Kubernetes to Low Carbon-netes: Optimizing K8s Infrastructure and Workloads for Sustainability
https://www.wonderingchimp.com/content/files/2025/03/from-kubernetes-to-low-carbon-netes.mp3

With this article, however, I want to document the demo I had prepared for the Heapcon talk. Here we'll go into a deeper explanation of the things I used for the demo, and how I set everything up on my local machine. Feel free to scroll down to the bottom for the link to the repository where I put everything demo-related.

Overview

Every good 8 seconds cooking recipe starts with an overview of the dish you're going to prepare. I will try to do a similar thing with the following screenshot. And I hope it will be enough to show you in a nutshell what I've created.

Split-screen terminal view comparing Kubernetes pod behavior under different carbon intensity conditions. Left panel shows K9s interface with one running pod (sample-app-8f46fb84f-7jdcr) and logs displaying "Carbon intensity updated: 331 gCO2eq/kWh" - representing high carbon conditions with minimal pod replicas. Right panel shows the same cluster scaled to three running pods with logs showing "Carbon intensity updated: 137 gCO2eq/kWh" - demonstrating increased pod replicas during lower carbon intensity periods. This visualization demonstrates carbon-aware autoscaling where Kubernetes dynamically adjusts workload replicas based on grid carbon intensity data.
Source: Local k8s setup

On the left-hand side you can see one replica of the resource and below its data about Carbon intensity. On the right-hand side, you can see three replicas, while the Carbon intensity is lower.

In a nutshell, here I'll show how I used data from Electricity Maps API and created a scaling point for resources, based on the overall Carbon intensity of the grid.

Ingredients

Keeping the 8-second cooking recipe story from above alive, in the next bullet points I'll show you what I used and to what extent.

  • 1 x Locally run k3s cluster.
  • 1 x Prometheus deployment (kube-prometheus-stack).
  • 1 x KEDA installation and configuration.
  • 1 x Electricity Maps API account.
  • 1 x Simple Node application.

Locally run k3s cluster

The k3s cluster is a light-weight implementation of Kubernetes, that you can easily spin up on your machine by following quite thorough instructions on the official k3s website.

Since I needed the cluster only for the demo purposes, I followed the quick start below.

Quick-Start Guide | K3s
This guide will help you quickly launch a cluster with default options. Make sure your nodes meet the requirements before proceeding.

Prometheus deployment

Now, this was a no-brainer for me. I've been using quite extensively the Prometheus/Grafana monitoring stack on the Kubernetes, therefore I've installed a default setup of kube-prometheus-stack helm chart.

This is a collection of Kubernetes manifests, Grafana dashboards, and Prometheus rules combined with Prometheus using Prometheus Operator. It was easy to follow and to set it up quickly.

helm-charts/charts/kube-prometheus-stack at main · prometheus-community/helm-charts
Prometheus community Helm charts. Contribute to prometheus-community/helm-charts development by creating an account on GitHub.

KEDA installation

In order to be able to scale on external events, I needed to add KEDA - Kubernetes Event Driven Autoscaler. In a nutshell, this tool will monitor your resources for any external events, say, Carbon intensity of the current power grid, and scale the resources based on the thresholds you've defined.

Again, documentation is quite good, and the whole setup was quite easy to follow from the following link.

KEDA | Deploying KEDA

Electricity Maps API Account

The Electricity Maps is the most useful platform where you can get the electricity data from. With the free account, you can get the relevant electricity data from one region. Which makes sense, because you can use this opportunity to test out the platform and see how can you use it in your workflow.

To create an account, just visit the following link, and follow sign up instructions.

Login | Electricity Maps

After you've created an account, you can play around on the platform. The most important part for us is the Developer Hub tab. Here you can find the data about your API authentication token, which you can use in the code.

Electricity Maps API Playground interface showing a test API request for carbon intensity data. The left panel displays configuration options including Data Type set to "Carbon Intensity", Temporality set to "Latest", Region tab selected with "Germany" chosen, and optional parameters for emission factor type set to "Lifecycle" with hourly temporal granularity. The right panel shows a curl request to the carbon intensity API endpoint with an authentication token (highlighted in green), and below it a JSON response displaying Germany's current carbon intensity of 324 gCO2eq/kWh along with timestamps and a test mode disclaimer noting the data is intentionally inaccurate for integration testing purposes. The interface is in dark mode with blue accent highlighting indicating test mode is active.
Source: Electricity Maps Developer Portal

Simple Node application

The last step in the process is to create an application that can use this data. I've created a simple JavaScript server that takes the data from the Electricity Maps on latest Carbon Intensity, logs the numbers to standard output, and exports the values as Prometheus metrics.

This is an important step, because the scaling will depend on metrics from this service.

The application is located in the repository under /app/server.js directory.

I've created a docker image, and deployed the application as a Deployment in a default namespace.

Mixing it all together

Having deployed everything from the above will bring us two steps closer to the solution from the beginning.

There are two steps that we're missing here:

  1. ServiceMonitor - a resource that tells Prometheus to scrape the metrics from the sample-app:80/metrics URL.
  2. ScaledObject - a resource that tells KEDA what to scale and on what grounds. This is the part I was having troubles the most.

Configuring correct scaling

The ScaledObject resource is quite powerful and if not set up correctly, it can lead to various problems. For example - whenever one request goes to the application, and the resources increase for just a bit, workload is scaled to n replicas.

In my example, I wanted to scale up on lower Carbon intensity, and scale down on higher Carbon intensity numbers. This required me to use the inverted logic on the ScaledObject. Because, by default, KEDA expects that when some numbers increase, it considers increasing the number of replicas.

Below you can see the example of the ScaledObject I used.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: carbon-intensity
  namespace: default  # Adjust to your namespace
spec:
  scaleTargetRef:
    name: sample-app  # Replace with your deployment name
  minReplicaCount: 1
  maxReplicaCount: 10
  triggers:
    - type: prometheus
      metadata:
        serverAddress: http://prometheus-kube-prometheus-prometheus.prometheus.svc.cluster.local:9090
        metricName: carbon_intensity_green_energy
        threshold: '50'
        activationThreshold: '1'
        # Inverted logic: higher value when carbon intensity is lower (greener)
        query: (250 - avg(carbon_intensity_gco2_per_kwh))

As you can see on the example above, the inverted logic is handled by the query option. This is how it will work:

  • When Carbon intensity = 150gCO2eq/kWh - query returns (250-150) = 100 -> Scale to ceil(100/50) = 2 replicas.
  • When Carbon intensity = 300gCO2eq/kWh - query returns (250-300) = -50 -> KEDA treats this as 0 and scales it to minReplicaCount = 1 replica.

Summary

It was quite easy for me to set this up by following quick-start guides, which are so good these days. However, this might and will differ from your use case. Fortunately, Electricity Maps API offers quite a lot of different data that you can use and act upon. In my example, I've used only basics.

For more information about the code and infrastructure I've deployed, check out the following repository.

GitHub - alternaivan/co2eq-autoscale-demo: co2eq-autoscale-demo
co2eq-autoscale-demo. Contribute to alternaivan/co2eq-autoscale-demo development by creating an account on GitHub.

Hopefully you can test it out yourself and can provide some feedback on the topic!

See you in the next article!