CI is a climate problem nobody's talking about
I spend a lot of time thinking about two things that don't usually appear in the same sentence: developer tooling and climate change. My About page lists both. People mostly assume those are separate hobbies. They aren't. The more I've looked, the more the two worlds keep colliding — and continuous integration is the most obvious collision point that the industry is almost completely ignoring.
Here's what I mean. GitHub Actions alone runs somewhere north of two billion compute-minutes per month. That is not a typo. Every push, every pull request, every scheduled nightly build spins up a runner somewhere in a data center, burns electricity, and spins back down. The runs are brief, but they are relentless, and they compound.
Data centers have gotten a lot of attention in the climate conversation, mostly through the lens of AI training runs and hyperscaler energy consumption. That framing is accurate but it creates a blind spot: the unglamorous, always-on, distributed compute that is just quietly ticking away on every repo in the world. CI runners aren't making headlines. They're making carbon.
The frustrating part is that a meaningful fraction of this is unnecessary — not the compute itself, but when the compute happens. Grids are not uniformly dirty. The carbon intensity of electricity varies by a factor of three to ten within a single day depending on what generation sources are online. A compute job that runs at 2 pm when the grid is coal-heavy produces dramatically more emissions than the same job run at 4 am when solar and wind are carrying the load. We know this. We just haven't connected it to the CI dashboard yet.
Carbon intensity signals
Carbon intensity is measured in grams of CO₂ equivalent per kilowatt-hour (gCO₂eq/kWh). It tells you how dirty the electricity you're consuming is at a given moment, in a given place. A lower number is better. Much lower is much better.
The variation is large enough to matter. On a typical day in California, grid intensity swings from around 130 gCO₂eq/kWh in the early afternoon — when demand is high and gas peakers are running — down to roughly 50 gCO₂eq/kWh around midday when solar output peaks. That's a 60% difference in emissions for identical compute, just by choosing when to run it. And that's a relatively clean grid. Poland's grid runs above 600 gCO₂eq/kWh for much of the day, mostly coal. Norway's sits below 30 gCO₂eq/kWh nearly always, hydropower. Region selection matters even more than timing within a region.
Two APIs make this signal queryable in real time:
- Electricity Maps — probably the most complete dataset, covering 50+ regions worldwide. Free tier is limited but sufficient for most CI use cases. Returns live intensity and a 24-hour forecast, which is the piece that makes scheduling practical.
- WattTime — US-focused but extremely granular, down to the utility level. Their marginal emissions signal is more precise than average intensity for determining whether your next unit of compute is displacing gas or renewables.
Both APIs return a number and, with a paid tier, a forecast. The forecast is the part that actually lets you schedule. You don't need to guess whether tonight will be low-carbon; you can ask.
Shifting jobs to low-carbon windows
The key insight is that not everything in your CI pipeline needs to run right now. There is a spectrum.
At one end: PR checks. Someone opened a pull request and is waiting on feedback. They need the tests to pass before they can merge. This is time-sensitive — delaying it by six hours to wait for a better grid window would make you incredibly unpopular with your team, and rightly so. Don't touch these.
At the other end: nightly builds, release artifact compilation, cache warming, dependency update bots, security scans, code coverage aggregation, performance regression benchmarks run outside the critical path. None of these have a human waiting on them right now. They just need to have run by morning. That is a multi-hour window of scheduling flexibility, and it is exactly what carbon-aware scheduling needs to do its job.
In between, you have things like staging environment refreshes, documentation regeneration, integration test suites that only run on merge to main. These need judgment. Does "within two hours of the merge" satisfy the requirement? Usually yes. "Within eight hours" often works too, which opens the window considerably.
Practically speaking, the audit is straightforward. Go through your workflow files and ask one question for each job: Is there a person or a downstream system that is blocked on this result within the next four hours? If yes: time-sensitive, leave it alone. If no: time-flexible, schedule it to run when the grid is clean. Most teams discover that 30–40% of their compute falls into the time-flexible bucket. That's the low-hanging fruit, and you can get to it without changing anything about how PRs or deploys work.
What the tooling looks like today
I want to be honest here: the tooling is immature. This is an area where the ideas are well ahead of the platform support.
GitHub Actions does not natively support carbon-aware scheduling. There is no schedule-when: low-carbon syntax. You cannot point a workflow at a region and ask it to run during the next clean window. This gap is real, and I'd love to see GitHub close it — they already pick the region for hosted runners, and they have the data to make smarter choices. But as of now, it's not there.
The workaround is a scheduled workflow that queries a carbon API and decides whether to proceed. This is clunky but functional. Here is what it looks like in practice:
# .github/workflows/nightly-build.yml
name: Nightly build (carbon-aware)
on:
schedule:
# Check every 2 hours between 10 PM and 6 AM UTC
- cron: '0 22,0,2,4,6 * * *'
workflow_dispatch:
inputs:
force:
description: 'Skip carbon check and run immediately'
type: boolean
default: false
jobs:
carbon-gate:
runs-on: ubuntu-latest
outputs:
proceed: ${{ steps.check.outputs.proceed }}
steps:
- name: Check grid carbon intensity
id: check
env:
ELECTRICITY_MAPS_TOKEN: ${{ secrets.ELECTRICITY_MAPS_TOKEN }}
run: |
# Query current intensity for the runner region (us-east-1 ~ PJM zone)
INTENSITY=$(curl -s \
-H "auth-token: $ELECTRICITY_MAPS_TOKEN" \
"https://api.electricitymap.org/v3/carbon-intensity/latest?zone=US-NY-NYIS" \
| jq -r '.carbonIntensity')
echo "Current grid intensity: ${INTENSITY} gCO2eq/kWh"
# Threshold: run if intensity is below 200 gCO2eq/kWh
# Adjust this to match your grid and ambition level
THRESHOLD=200
if [ "$(echo "$INTENSITY < $THRESHOLD" | bc)" = "1" ] || \
[ "${{ inputs.force }}" = "true" ]; then
echo "proceed=true" >> "$GITHUB_OUTPUT"
echo "Grid is clean enough (${INTENSITY} < ${THRESHOLD}). Running."
else
echo "proceed=false" >> "$GITHUB_OUTPUT"
echo "Grid too dirty (${INTENSITY} >= ${THRESHOLD}). Skipping this window."
fi
nightly-build:
needs: carbon-gate
if: needs.carbon-gate.outputs.proceed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run expensive build steps
run: |
npm ci
npm run build:full
npm run test:coverage
npm run build:artifacts
The pattern: a lightweight gate job queries the API and sets an output. The expensive job only runs if the gate passes. If the intensity is too high this window, the workflow exits cleanly and the next scheduled run picks it up two hours later. You add a workflow_dispatch with a force input so any engineer can override the gate when they genuinely need to run immediately — which preserves the escape valve and reduces friction.
Beyond this pattern, the ecosystem is sparse. The grid-intensity npm package wraps several carbon APIs into a consistent interface and is worth knowing about if you're scripting this in Node. For Kubernetes workloads, Kepler instruments pods to report actual energy consumption, which gives you measurement data to work with. Neither of these is a complete solution; they're building blocks.
The honest assessment: right now you are gluing things together by hand. The upside is that it works, and the core pattern — check intensity, conditionally proceed — is simple enough to implement in any CI platform. The downside is that it requires maintenance, and the thresholds are somewhat arbitrary without good historical data for your specific region. This will improve. The Green Software Foundation and a growing number of cloud providers are moving toward first-class carbon-aware APIs. The primitives exist; the integration is catching up.
What you can do today
Three steps, roughly in order of effort and return:
1. Audit which of your jobs are time-flexible. This takes an hour and costs nothing. Open your .github/workflows/ directory (or equivalent) and categorize every workflow. Time-sensitive: PR checks, deploy pipelines, anything with a human waiting. Time-flexible: everything else. Write it down. Even if you never implement a single line of carbon-aware scheduling, the audit is valuable — it forces you to think about which compute is actually urgent and which is running on autopilot because nobody questioned the cron schedule. Most teams find 30–40% of their CI is time-flexible. That is the ceiling on what carbon-aware scheduling can accomplish for you, and knowing that number tells you whether it's worth investing further.
2. Add a carbon signal check to your nightly and scheduled builds. Pick one workflow that is clearly time-flexible — the nightly build, the weekly dependency update run, the monthly artifact release. Implement the gate pattern from the code example above. Sign up for an Electricity Maps free-tier key. Set a conservative threshold (200 gCO₂eq/kWh is a reasonable starting point for most US regions). Run it for a month and observe how often it defers versus proceeds. This gives you real data on what the impact would look like at scale across your whole pipeline, before you've committed to rolling it out broadly.
3. Pick a low-carbon region for your self-hosted runners. If you run self-hosted CI on a cloud provider, region selection is the single highest-leverage move available to you — no API integration required. us-west-2 (Oregon, backed by significant hydro and wind) consistently outperforms us-east-1 on carbon intensity. eu-north-1 (Stockholm) is the cleanest AWS region in Europe by a wide margin. If your latency requirements are flexible, this is a free lunch. Check your cloud provider's sustainability reports — most of them publish region-level carbon data now, and some publish it at the API level.
None of these steps add time to your merge process. Time-sensitive jobs are untouched. The engineering cost is low. The emissions reduction, at scale across the industry, is not.
I'm aware this is the part of the post where it could tip into preachiness, so let me say it plainly instead: I don't think developers are morally obligated to care about this. But I do think it's worth knowing that the always-on compute hum underneath our tooling has a physical footprint, that the footprint is unevenly distributed across grid regions and times of day, and that with a modest amount of engineering effort it can be meaningfully smaller. The tooling will get easier. The underlying physics won't change.
When I ride a long route on the weekend and I'm grinding up a climb, I sometimes think about the fact that the PR I just merged at 2 pm might have run on dirtier electricity than it needed to. That's probably an occupational hazard. But it's also a fixable problem, and I'd rather spend twenty minutes plumbing a carbon gate into a cron job than just thinking about it on a hill.
— Hendrik