Shifting batch jobs to cleaner hours: what actually moves the needle
Carbon-aware scheduling is simple to build and easy to oversell. Notes from a year of doing it.
Contents
Our product shifts EV charging to the hours when the grid is cleaner, so naturally people on my team asked whether we do the same with our own batch jobs. We do now. Here is what I learned, including where the gains are smaller than the conference talks suggest.
The basic idea
Electricity is not equally clean every hour. When wind and solar are high, the grid mix has a lower carbon intensity (grams of CO2 per kWh). If a job is flexible, a nightly training run, a big report, a backup, you can move it to a cleaner window before its deadline. That is temporal shifting. Moving the work to a region with a cleaner grid is spatial shifting, and it comes with its own trade-offs around latency, data residency and egress costs.
Average vs marginal intensity
This is the part that usually gets skipped. Average intensity is the mix of everything generating right now. Marginal intensity is the emissions of the plant that ramps up when you add load. For a decision like "start this job now or at 3am", marginal is the more honest signal, because your extra load is served by whatever is at the margin, and that is often gas. Marginal data is modelled rather than measured, so be humble about its precision. We use average intensity for reporting and a marginal forecast for scheduling decisions, and we always write down which one a number is.
The scheduler is the easy part
Ours is about forty lines. Each job declares an earliest start, a deadline and an expected duration. We fetch a 24 hour forecast and pick the start time that minimizes the mean forecast intensity over the job's duration, inside the window. If the forecast is missing, the job runs immediately. Never let a carbon optimisation be the reason a deadline is missed, or the team will rightly turn it off. Roughly:
starts = candidate_starts(earliest, deadline - duration, step=timedelta(minutes=15))
best = min(starts, key=lambda t: mean_intensity(forecast, t, t + duration))
Where the gains are real, and where they are not
- It depends a lot on your grid. In Sweden the grid is already low carbon and fairly flat, so moving a job by a few hours changes little. On grids with a lot of solar and evening gas peaks, the difference between noon and 7pm can be large.
- Flexible load is often a small share. The always-on services, the API that serves users, do not shift. Be honest about what percentage of your compute is actually movable before you announce anything.
- Do not all pile into the same hour. If every job picks 3am you create a new peak and maybe need more capacity, which has embodied emissions of its own. We add a small random spread inside the best window.
- Efficiency still wins first. Deleting a job nobody reads saves 100% of its emissions. We found two of those while doing this. It was the best result of the whole project, and nobody writes conference talks about it.
I will share how we report this internally in another post. The short version: kWh first, emissions second, and always show the uncertainty in the same time as the number.
Written by
Emma Lindqvist
Software engineer at a climate tech company in Stockholm, building tools that shift EV charging to when the grid is cleaner. Python, time series, cold-water swimming.
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