Software engineers have a largely untapped opportunity to contribute to a greener world. In this article, I discuss how we can schedule asynchronous tasks to minimise carbon emissions.
Electricity comes from various sources, each with different carbon emissions. Depending on the source, a certain amount of carbon is emitted for each generated kilowatt hour (kwh). Clean and renewable sources like wind, solar, and hydroelectric power emit minimal carbon, while fossil fuel sources emit quite a bit more. The amount of carbon emitted by an energy source is called its carbon intensity, measured in grams of carbon per kwh.
Ideally, you would plug your computer or data center directly into a wind farm so that your software is powered with zero carbon intensity. However, it is not feasible for most individuals to directly connect to wind farms; instead, they rely on power grids that receive electricity from a mix of sources. Once connected to a grid, you cannot control the specific sources supplying your electricity. You simply receive a blend of all the current power sources within the grid, including both lower- and higher-carbon sources. Therefore, the carbon intensity of the electricity you consume reflects a combination of all the sources on the grid at that moment.
Carbon awareness is the idea of doing more when more energy comes from low-carbon sources and doing less when more energy comes from high-carbon sources. One approach to building carbon-aware software is called demand shaping: we shape our computation to match the existing supply. When the carbon cost of running your application becomes high, shape the demand to match the supply of carbon.
Demand shaping sounded a bit abstract to me when I first heard about it. Therefore, I decided to build carbon-aware software myself using Kotlin and Spring Boot. This article shows my plans and insights. You can find my code here at GitHub.
Our implementation
We'll simulate a scenario where a producer periodically puts task descriptions on a queue, describing a computationally intensive task that some consumer should execute asynchronously. Imagine this being some tasks that are not time-critical but should be performed somewhere in the future. Before executing a task, the consumers will first reach out to an API called the CarbonAwareSDK, which provides information about the carbon intensity at a certain time and place.
Each consumer implements a different strategy for processing events based on the retrieved carbon intensity:
- Threshold consumer only consumes new tasks from the queue if the carbon intensity is lower than a certain threshold.
- Adaptive velocity consumer, which takes longer breaks between tasks based on the carbon intensity.
- Forecast-based consumer, which runs its daily task at the optimal time in the next 24 hours.
CarbonAwareSDK API
CarbonAwareSDK helps us retrieve information about the carbon intensity at a certain time and place. Documentation on the available endpoints is found here. For our purposes, we use two of its functionalities.
Current Carbon Intensity
We can request the current carbon intensity at a specific location through the following URL:
https://carbon-aware-api.azurewebsites.net/emissions/bylocation?location=eastus
The response contains a field rating
: the carbon intensity, measured in grams of CO2 equivalent per kilowatt hour.
[
{
location: "PJM_ROANOKE",
time: "2023-06-26T17:05:00+00:00",
rating: 588.30930389,
duration: "00:05:00"
}
]
Forecasted carbon intensity
We can also ask the service to forecast carbon intensities between two timestamps, indicated by path variables dataStartAt
and dataEndAt
.
https://carbon-aware-api.azurewebsites.net/emissions/forecasts/current?location=northeurope&dataStartAt=2023-07-19T15:00:00Z&dataEndAt=2023-07-20T16:00:00Z&windowSize=10
One really cool feature is that it also gives us the most moment in that time frame where carbon intensity is lowest.
[
{
generatedAt: "2023-06-26T17:00:00+00:00",
requestedAt: "2023-06-26T17:03:33.4552082+00:00",
location: "eastus",
dataStartAt: "2023-06-26T17:05:00+00:00",
dataEndAt: "2023-06-27T17:00:00+00:00",
windowSize: 0,
optimalDataPoints: [
{
location: "PJM_ROANOKE",
timestamp: "2023-06-27T07:45:00+00:00",
duration: 5,
value: 497.9432056032178
}
],
forecastData: [
{
location: "PJM_ROANOKE",
timestamp: "2023-06-26T17:05:00+00:00",
duration: 5,
value: 607.0114067773352
},
{
location: "PJM_ROANOKE",
timestamp: "2023-06-26T17:10:00+00:00",
duration: 5,
value: 606.2321824825342
},
{
location: "PJM_ROANOKE",
timestamp: "2023-06-26T17:15:00+00:00",
duration: 5,
value: 605.3570016963745
},
... and more ...
]
}
]
Threshold consumer
This first consumer is called every 5 seconds using Spring Boot's @Scheduled
annotation. Upon invocation, the current carbon intensity is retrieved. Tasks are only retrieved and executed if the carbon intensity is lower than the threshold MAXIMUM_CARBON_INTENSITY
.
@Component
class ThresholdConsumer(
val taskProcessor: TaskProcessor,
val carbonIntensityService: CarbonIntensityService,
val queueService: QueueService
) {
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
fun consume() {
val currentCarbonIntensity = carbonIntensityService.getIntensity().rating
if (currentCarbonIntensity < MAXIMUM_CARBON_INTENSITY) {
val task = queueService.receiveTask()
task?.let {
val result = taskProcessor.process(it)
LOGGER.info("Task ${it.id} executed successfully with threshold strategy. Result = ${result}.")
}
} else {
LOGGER.info("No task executed with threshold strategy because carbon intensity ${currentCarbonIntensity} is too high (threshold = ${MAXIMUM_CARBON_INTENSITY}).")
}
}
companion object {
private const val MAXIMUM_CARBON_INTENSITY = 400
private val LOGGER = LoggerFactory.getLogger(ThresholdConsumer::class.java)
}
}
Adaptive velocity consumer
The adaptive velocity consumer is different from our first consumer, who will always continue consuming messages. It just works harder when the carbon intensity is lower. After reading and executing a task, the consumer sleeps 10 milliseconds for each gram of CO2 emitted for a kwh at this moment. It will therefore sleep less if more clean energy is available.
@Component
class AdaptiveVelocityConsumer(
val taskProcessor: TaskProcessor,
val carbonIntensityService: CarbonIntensityService,
val queueService: QueueService
) {
@EventListener
fun consume(event: ApplicationReadyEvent) {
while (true) {
val currentCarbonIntensity = carbonIntensityService.getIntensity()
val delay = currentCarbonIntensity.rating * 10L
Thread.sleep(delay.toLong())
val task = queueService.receiveTask()
task?.let {
val result = taskProcessor.process(it)
LOGGER.info("Task ${it.id} executed successfully with adapted speed strategy. Result = ${result}.")
}
}
}
companion object {
private val LOGGER = LoggerFactory.getLogger(AdaptiveVelocityConsumer::class.java)
}
}
Forecast consumer
The first two consumers make decisions based on the current carbon intensity, which is quite similar to a greedy algorithm. Our final consumer aims to be smarter than that. Instead of asking CarbonAwareSDK for the current carbon intensity, the consumer requests a forecast for the coming day. The response looks as below. The most important field is optimalDataPoints
, which contains the best moment to perform our task.
We schedule the consumer to be run once per day. During invocation, it retrieves the best moment to execute a task that day and schedules it using Spring Boot's TaskScheduler
.
@Component
class ForecastingConsumer(
val taskScheduler: TaskScheduler,
val taskProcessor: TaskProcessor,
val carbonIntensityService: CarbonIntensityService,
val queueService: QueueService
) {
@Scheduled(fixedDelay = 1, timeUnit = TimeUnit.DAYS)
fun consume() {
val task = queueService.receiveTask()
task?.let {
val optimalIntensity = carbonIntensityService.getForecastedOptimalIntensity().optimalDataPoints.first()
taskScheduler.schedule(Instant.from(DateTimeFormatter.ISO_INSTANT.parse(optimalIntensity.timestamp))) {
val result = taskProcessor.process(it)
LOGGER.info("Task ${it.id} executed successfully with forecasting strategy. Result = ${result}.")
}
LOGGER.info("Task ${it.id} scheduled successfully at ${optimalIntensity.timestamp} with forecasting strategy.")
}
}
companion object {
private val LOGGER = LoggerFactory.getLogger(ForecastingConsumer::class.java)
}
fun TaskScheduler.schedule(moment: Instant, task: Runnable) =
this.schedule(task, moment)
}
Insights
These three consumers give us an initial idea of how we can build carbon-aware software. However, is the application level the right place to implement this logic? Yes, we manage to do more work when we emit less carbon, but we still have these applications running and methods being invoked. All these network requests between the consumers and the CarbonAwareSDK also costs electricity. In addition, the application becomes more complex with this shaping logic, degrading its carbon efficiency. The three consumers each also have specific downsides:
- The threshold consumer cannot guarantee that tasks are ever executed. Some locations in the world are still very reliant on coal so that the threshold might be too low.
- The adaptive velocity consumer reads faster when cleaner energy is available but could still process a lot of its tasks during very dirty times of the day. Even worse, it could slowly work through the whole queue during high carbon intensity hours and then have nothing left to do when it wants to speed up.
- The forecasting consumer solves the downsides of the previous two, but scheduling a task at the application level is a risky move. At any moment, the application could be killed as part of a rolling update or some error unrelated to the task. The scheduling is then lost. The task description was already removed from the queue, so it won't be executed anymore altogether.
This all gets even messier if we have multiple replicas of the same application, which is not uncommon in production environments.
Shift down to the platform
I believe the forecasting consumer is the most promising of the three strategies. However, the application is not the right place to implement this functionality. Instead, I imagine having a custom resource in Kubernetes that allows us to schedule tasks based on the carbon intensity.
apiVersion: carbonaware/v1
kind: CarbonAwareDailyJob
metadata:
name: my-day-job
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: consumer
image: task-consumer:1.0
restartPolicy: OnFailure
This CarbonAwareDailyJob
custom resource and its associated custom controller, reach out to the CarbonAwareSDK (which you can also host on-premise!) for the forecast of the next 24 hours. The Job described by the jobTemplate
is then executed at that optimal moment, very similar to the forecasting consumer we saw earlier. An added benefit is that our custom controller centralises all calls to the CarbonAwareSDK API, saving network traffic and carbon.
Conclusion
Implementing carbon-aware software with demand shaping techniques has the potential to reduce carbon emissions and promote sustainability in our digital landscape. In this article, we applied demand shaping at the application level. However, I learned that demand shaping might be better implemented at a lower level. Green software patterns like demand shaping are still under active development. Continued research, collaboration, and adoption of carbon-aware software solutions are vital to realising these benefits and creating a greener future.
Top comments (0)