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.
Contents
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
Sign in to join the conversation.
Sign in to comment