Skip to content
article

We moved our nightly jobs to the greenest hours of the grid. Here's what it saved (spoiler: less than you'd think, and that's the point)

Real numbers from a small co-op: 0.27 kWh a night, a free API, about 40 lines of Python, and why the big win was not the servers at all.

Freya Collins@freya_bristol

September 9, 2026 · 5 min read

I'm a data analyst at a community energy co-op in Bristol. We're small: about 400 member households, a few solar roofs, a share in a wind turbine, and one Linux box in a rack in a shared data centre that runs all our nightly jobs (meter data imports, billing calcs, the dashboards members look at). People in this topic keep asking about carbon-aware scheduling, so here's an honest write-up of doing it for real, including the bit where the numbers are embarrassingly small.

The idea

Grid electricity isn't equally dirty all day. In Great Britain the carbon intensity (grams of CO2 per kWh) swings a lot depending on how much wind and solar is on the system versus gas. On a windy night it can be under 50 g/kWh. On a still, cold evening at 6pm it can be well over 200. If a job doesn't care exactly when it runs, run it when the grid is cleanest.

The data

National Grid's Carbon Intensity API is free, needs no key, and gives you a 48 hour forecast in half-hour slots, including by region or postcode. Regional matters: Scotland and the south west can look very different on the same night.

The code

This is close to what runs on our box every evening at 18:00. It finds the cleanest consecutive window of the length the job needs, inside the window we're allowed to run in (overnight, so it's done before members wake up and look at the dashboards).

import requests
from datetime import datetime, timedelta, timezone

API = "https://api.carbonintensity.org.uk/regional/intensity/{start}/fw48h/postcode/{pc}"
FMT = "%Y-%m-%dT%H:%MZ"

def greenest_start(postcode="BS3", slots=3, earliest_h=22, latest_end_h=6):
    """Cleanest run of slots half-hours, starting after earliest_h tonight
    and finished by latest_end_h tomorrow (all UTC). Returns (start, avg g/kWh)."""
    now = datetime.now(timezone.utc)
    r = requests.get(API.format(start=now.strftime(FMT), pc=postcode),
                     headers={"Accept": "application/json"}, timeout=15)
    r.raise_for_status()
    body = r.json()["data"]
    region = body[0] if isinstance(body, list) else body   # shape differs between endpoints
    periods = region["data"]

    start_ok = now.replace(hour=earliest_h, minute=0, second=0, microsecond=0)
    end_ok = (start_ok + timedelta(days=1)).replace(hour=latest_end_h)

    best = None
    for i in range(len(periods) - slots + 1):
        window = periods[i:i + slots]
        t0 = datetime.strptime(window[0]["from"], FMT).replace(tzinfo=timezone.utc)
        t1 = datetime.strptime(window[-1]["to"], FMT).replace(tzinfo=timezone.utc)
        if t0 < start_ok or t1 > end_ok:
            continue
        avg = sum(p["intensity"]["forecast"] for p in window) / slots
        if best is None or avg < best[1]:
            best = (t0, avg)
    return best

if __name__ == "__main__":
    t0, avg = greenest_start()
    print(t0.strftime("%Y-%m-%d %H:%M:%S UTC"), round(avg))

A tiny wrapper takes that output and creates a one-off systemd timer for the job:

WHEN=$(python3 /opt/coop/greenest.py | cut -d' ' -f1-3)
systemd-run --unit=nightly-etl-$(date +%F) \
  --on-calendar="$WHEN" \
  /opt/coop/run_nightly.sh

If the API is down, the wrapper falls back to 02:00, which on average is a decent hour anyway. Boring fallback, no drama.

The numbers

We ran it for June, July and August and compared against the old fixed 02:00 start, using the API's actual (not forecast) intensity for each slot we ran in.

  • The nightly job takes about 90 minutes and the box draws roughly 180 W while it's working. So about 0.27 kWh a night.
  • Average intensity at the old 02:00 start: 128 g/kWh. Average in the chosen windows: 71 g/kWh.
  • Saving: 0.27 kWh × 57 g ≈ 15 g of CO2 a night. About 1.4 kg over the whole summer.

1.4 kg. That's roughly one short drive to the shops and back. For our servers, carbon-aware scheduling is a rounding error, and I'd rather say that out loud, because a lot of "green software" marketing quietly pretends otherwise.

Where it actually matters

The interesting part came after. The same 40 lines, pointed at members' flexible loads, are a completely different story. About 90 of our households have a heat pump hot water cylinder or an EV charger they've let us schedule (with an override button, always). We shift around 3 kWh per household per day out of the evening peak and into the cleanest window.

  • Typical evening-peak intensity for those loads: around 190 g/kWh. Chosen windows: around 75.
  • 3 kWh × 115 g × 90 households ≈ 31 kg of CO2 a day, roughly 2.8 tonnes over the summer.
  • And because the cleanest hours are usually the cheapest on a time-of-use tariff, members on those tariffs saved money too. Median member: about £6 a month.

Same code. Two thousand times the impact. The lesson for me: carbon-aware scheduling is worth doing where the energy is, and in most organisations that isn't the nightly cron job. It's heating, cooling, charging, and big batch compute like model training. If your nightly ETL is the only thing you can shift, do it, it's free, but don't put it in the annual report.

Caveats, because someone will ask

  • This is average (not marginal) intensity, which is what the API gives. Marginal emissions would be the more correct measure for "what does my extra load cause", and it's harder to get. Pointers welcome.
  • Shifting load only helps if everyone doesn't pile into the same window. At our scale, not a problem. At national scale, that's literally what the grid operator worries about.
  • Our data centre buys "100% renewable" via certificates. I don't count that, because the physical electrons at 6pm still come from gas.

Happy to share the full script and the member scheduling bits. tbh the hardest part wasn't the code, it was the override button UX.

Written by

Freya Collins

Data analyst at a community energy co-op 🌱 Python, SQL, too many dashboards. Bristol. Cycles everywhere. Carbon-aware computing nerd.

Get the best of #carbon_aware weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

17

Sign in to join the conversation.

Sign in to comment
Gary Pritchard@contractor_gaz#3 in Carbon Aware
1.4 kg. ONE POINT FOUR KG. All that python to save less than a bag of sugar while my energy bill went through the roof thanks to net zero. absolute joke mate
5 net votes
Freya Collins@freya_bristol#1 in Carbon Aware
Gaz, I literally wrote that the server saving is a rounding error. That was the point of the post. The 2.8 tonnes and the £6 a month for members is the bit that matters. And your bill went up mostly because of wholesale gas prices after 2021, not because of wind farms. I can send you the price cap breakdown again 😅
5 net votes
Gary Pritchard@contractor_gaz#3 in Carbon Aware
and the standing charges? and the policy costs? convenient that the breakdown never has those in bold
-1 net votes
Freya Collins@freya_bristol#1 in Carbon Aware
Policy costs are on the breakdown, they're a slice, not the cake. Happy to make you a table. You'll ignore it, but I'll make it.
1 net votes
Gary Pritchard@contractor_gaz#3 in Carbon Aware
make the table. i'll read it with a beer. still voting the same way
5 net votes
Bram Verhoeven@bram_on_a_bike#5 in Carbon Aware
Good post. We do this at work for the analytics batch on Kubernetes, a CronJob that checks the forecast first. Same conclusion. Our biggest saving was turning off the staging cluster at night. Nothing clever.
5 net votes
Lindiwe Mokoena@loadshedding_lindi#5 in Carbon Aware
honest numbers, love it. our grid in SA is mostly coal, so the "greenest hour" is still filthy and carbon-aware is a bit of a joke here. but turning off idle stuff, ja, that works everywhere
4 net votes
Imran Hussain@imran_builds#5 in Carbon Aware
Gaz reading a table with a beer is the most progress this thread has seen in months. respect to both of you
10 net votes
Emma Lindqvist@emmalindqvist#2 in Carbon Aware
Great write-up. On marginal vs average: WattTime publishes marginal emissions signals, though the free tier is limited and the methodology debates are endless. For GB I've seen people approximate marginal as "gas whenever gas is on the margin", which is most hours. Saying the server part is a rounding error is the most honest thing I've read in this topic.
4 net votes
Freya Collins@freya_bristol#1 in Carbon Aware
thank you! will look at WattTime. and yes, "gas on the margin most hours" is roughly what our grid people assume too tbh
1 net votes
Sanne Bakker@sanne_sre#5 in Carbon Aware
Turn things off. Then schedule. In that order.
3 net votes
Rhys Llewellyn@rhys_teaches_code#5 in Carbon Aware
Freya this is going straight into my Year 12 lesson on data and APIs. Real data, real code, and an honest result. Tidy. Diolch! (and @henning_hh will want the heat pump bit)
2 net votes
Wolfgang Engel@dsgvo_wolf#5 in Carbon Aware
Very good. I would like to point out one thing about the member scheduling: the household load profile is personal data under Art. 4 GDPR, and a time-of-use profile can reveal when people are at home. I assume you have a DPIA for it. 🙂
7 net votes
Freya Collins@freya_bristol#1 in Carbon Aware
we do! the DPIA was longer than the code. and members opt in per device, with the override button, which is part of why the button UX took so long
5 net votes
Ryan Goh@ai_maxi_ryan#5 in Carbon Aware
or just put the servers in a cheaper region and let AI optimise it lol. 40 lines of python for 1.4kg 🤖
5 net votes
Freya Collins@freya_bristol#1 in Carbon Aware
did you read past the first number?
-2 net votes
Henning Brockmann@henning_hh#5 in Carbon Aware
Very interesting, and correct that the server part is small. Our heat pump in Hamburg uses in the winter months 18 to 25 kWh per day, so the flexible part is where the effect is. We shift the hot water to the midday hours with PV, this is not so complicated like people think. The COP for hot water at midday is also better because the outside air is warmer, so a second effect.
6 net votes