Green scheduling without effort
September 2, 2026 ยท View on GitHub

Green scheduling without effort
This open-source job scheduler for Java developers revolutionizes the way you manage your tasks by dynamically assigning jobs to timeslots with greener energy. By effortlessly integrating this scheduler into your software, you can significantly reduce your carbon footprint and contribute to a more sustainable future. Embrace the power of green energy and make your software run greener with ease.
Let's look at an example. We have a job currently running at 9 o'clock that runs roughly an hour. From a business point of view we do not really care when it runs. As long as the job is finished before the end of the working day. So we could schedule it anywhere between 8:00 - 17:00.
The scheduler will calculate when the best (greenest) time is to run the job. For example on the day in the screenshot below, running at 11 o'clock would be more sustainable to run the job. (As the graph shows, there are even better spots outside the window)

Over a month we can see significant reductions in the carbon footprint of this job:

Current state
This scheduler uses carbon intensity data provided by the carbonintensity.io API โ a publicly available service developed by the carbonintensity.io team. Access to the API is free and requires an API key.
The scheduler is tested with Spring, Spring Boot and Quarkus for the NL (Netherlands) carbonIntensityZone. For known issues, planned improvements, and feature requests, please refer to the issues section.
Supported versions
Java, Quarkus and Spring Boot each move fast enough that "works with the version we happened to build against" isn't good enough - a scheduled compatibility check tests the published artifacts against the version lines below every week (plus JDK 25 and a canary check of whatever framework line is newest), so this table reflects what's actually verified rather than a guess:
| Required (blocks a release if broken) | |
|---|---|
| Java | 17, 25 |
| Quarkus | every active LTS line (currently 3.27 and 3.33) |
| Spring Boot | every active line of the current major, plus the last line of the previous major (currently 3.5, 4.0 and 4.1) |
A newer, not-yet-LTS Quarkus line is checked too, but as a canary: we want to know early if something's about to break, without blocking on a line nobody's committed to supporting yet. See compatibility/policy.yaml for the exact selection rules and docs/adr/0001-compatibility-testing-strategy.md for the reasoning behind them.
Micronaut support is new (CIIO-250) and entirely canary for now: every major line Micronaut's own release lifecycle still lists as active (currently 3, 4 and 5) is checked on every scheduled run, but a failure there doesn't block a release yet - nobody has committed to supporting Micronaut in production the way Quarkus and Spring Boot are supported above. It moves to the table above once the team has actually verified a line and made that call, the same path Spring Boot 4.0/4.1 went through.
How to build
The build instructions are available in the contribution guide.
Usage
Add the following dependency to the pom for a Spring Boot based project:
<dependency>
<groupId>io.carbonintensity</groupId>
<artifactId>green-scheduler-spring-boot-starter</artifactId>
<version>0.8.6</version>
</dependency>
or the following for a Quarkus-based project:
<dependency>
<groupId>io.carbonintensity</groupId>
<artifactId>quarkus-green-scheduler</artifactId>
<version>0.8.6</version>
</dependency>
or the following for a Micronaut-based project (experimental: the newest of the three extensions, not yet covered by the compatibility matrix below). Unlike the Spring Boot and Quarkus extensions, this one is split into a runtime and a build-time annotation-processor artifact that both need to be on the same version - import the BOM rather than pinning each one separately, so a future upgrade can't leave them out of sync:
Maven:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.carbonintensity</groupId>
<artifactId>green-scheduler-bom</artifactId>
<version>0.8.6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.carbonintensity</groupId>
<artifactId>green-scheduler-micronaut</artifactId>
</dependency>
</dependencies>
Gradle (Kotlin DSL):
dependencies {
implementation(platform("io.carbonintensity:green-scheduler-bom:0.8.6"))
implementation("io.carbonintensity:green-scheduler-micronaut")
}
The Micronaut extension processes @GreenScheduled at compile time, so
green-scheduler-micronaut-processor must also be added as an annotation processor path, next to
micronaut-inject-java. With the BOM imported above, its version can be omitted the same way
(requires Maven Compiler Plugin 3.11.0+; Gradle always resolves it from the platform):
Maven:
<path>
<groupId>io.carbonintensity</groupId>
<artifactId>green-scheduler-micronaut-processor</artifactId>
</path>
Gradle (Kotlin DSL):
dependencies {
annotationProcessor(platform("io.carbonintensity:green-scheduler-bom:0.8.6"))
annotationProcessor("io.carbonintensity:green-scheduler-micronaut-processor")
}
In the application.yaml add the following config including the API key:
green-scheduler:
api-key: { CARBONINTENSITY_API_KEY }
Then apply the @GreenScheduled annotation on the to be scheduled method. For an example annotation see the spring-boot-demo
Schedulers
There are 2 types of schedulers: fixed and successive.
Fixed
A scheduler in starting each day in the configured fixed window, where the actual start-time is calculated using the carbon intensity on the set carbonIntensityZone and the configured estimated duration of the job. Optionally, a timeZone can be configured in the annotation to specify in which timezone the start and end-time of the fixedWindow are. When no timezone is configured, it will default to the system-default timezone. Note that if the application is redeployed during this window, it might run again.
Using the "regular" scheduler:
@Scheduled(cron = "0 0 9 * * ?", zone = "Europe/Amsterdam")
public void runAt9AM() {
// Task to be executed at 9 AM every day
}
This would be replaced by the @GreenScheduled annotation. The job below might start at 8:00, 17:00 or anywhere in between, depending on the carbon intensity calculation. Also see Javadoc of @GreenScheduled.
@GreenScheduled(fixedWindow = "08:00 17:00", duration = "1h", carbonIntensityZone = "NL", timeZone = "Europe/Amsterdam")
public void greenFixedWindowJob() {
// Task to be started at the greenest moment between 08:00 and 17:00
}
Fixed on a specific day
The green scheduler can also run on a specific day. These days have the same notation as the fields day-of-Month and day-of-Week in a cron expression (Quartz implementation) and can not be used at the same time.
Using the "regular" scheduler:
@Scheduled(cron = "0 0 9 ? * MON", zone = "Europe/Amsterdam")
public void runAt9AMonMonday() {
// Task to be executed at 9 AM every monday
}
@Scheduled(cron = "0 0 9 1 * ?", zone = "Europe/Amsterdam")
public void runAt9AMFirstOfMonth() {
// Task to be executed at 9 AM every first of the month
}
Using the green scheduler:
@GreenScheduled(fixedWindow = "08:00 17:00", duration = "1h", carbonIntensityZone = "NL", timeZone = "Europe/Amsterdam", dayOfWeek= "MON")
public void greenFixedWindowJobMonday() {
// Task to be started at the greenest moment between 08:00 and 17:00
}
@GreenScheduled(fixedWindow = "08:00 17:00", duration = "1h", carbonIntensityZone = "NL", timeZone = "Europe/Amsterdam", dayOfMonth= "1")
public void greenFixedWindowJobFirstMonth() {
// Task to be started at the greenest moment between 08:00 and 17:00
}
Successive (experimental)
A scheduler starting at the lowest carbon intensity for the carbonIntensityZone within the configured gaps, keeping in mind the duration and carbonIntensityZone while calculating the optimal starting time of the job.
Using the "regular" scheduler to run a process every 3 hours:
@Scheduled(cron = "0 0 */3 * * ?", zone = "Europe/Amsterdam")
public void runEvery3Hours() {
// Task to be executed every 3 hours
}
Let's replace it by the @GreenScheduled annotation. In the example below an initial maximum gap of 3h, the scheduler will find the start-time with the lowest carbon intensity within the first 3 hours of deployment as initial starting point. To have a minimum gap of 1h and max of 4h between each consecutive run. Also see Javadoc of @GreenScheduled.
@GreenScheduled(successive = "3h 1h 4h", duration = "PT30M", carbonIntensityZone = "NL")
public void greenSuccessiveWindowJob() {
// Scheduled job process starting at the greenest moment in the specified window
}
Requesting an API key
Visit the carbonintensity.io homepage to get an API key for the scheduler.
Supported zones
The current supported list can be found soon on carbonintensity.io.
Concurrent executions
The scheduler may start a process multiple times when multiple instances of the same application are running (for example, on different nodes). To make sure that the process is only started once, use a solution such as ShedLock.
ShedLock is supported by and tested with green-scheduler release v0.8.3 and later, for Spring Boot and
Quarkus-based projects. For Spring Boot applications, please note that the deprecated TaskScheduler proxy mode of
ShedLock is not supported. ShedLock also offers official support for Micronaut, though this has not yet been
verified against the (experimental) Micronaut extension of green-scheduler.
Refer to the ShedLock documentation for more information on how to configure and use it with scheduled jobs:
- Instructions for Spring-based application can be found here.
- For Quarkus-based applications, use ShedLock's CDI integration.
- For Micronaut-based applications, use ShedLock's Micronaut integration.
Scheduling multiple jobs
Each @GreenScheduled job (Fixed or Successive) is planned independently, based only on its own window,
duration and carbonIntensityZone. By default, if several jobs within the same application instance target
the same zone and their optimal windows overlap, they may all be scheduled at the exact same, greenest moment
- there is no coordination between them out of the box.
Set green-scheduler.max-concurrent-per-slot to a positive number to limit how many jobs are allowed to start
at the exact same slot within the same zone. Jobs beyond that limit are automatically spread to the next-best
slot instead (taking each job's duration into account), and so on for further jobs, until a slot is found
that satisfies the limit.
This is disabled by default (0): existing applications are unaffected unless this property is explicitly
set, since changing when jobs fire is a deliberate scheduling decision, not something that should change
silently on upgrade.
A job's own configured window always takes priority over this limit: if every slot within the window is already at the limit, the job still runs at its greenest available slot in that window rather than not running at all (a warning is logged when this happens). This only coordinates jobs known to this application instance - it does not coordinate across multiple instances/replicas of the same application; combine it with ShedLock if you also run multiple instances.
Acknowledgements
The maven project structure and all documentation regarding contribution is adapted from what the Quarkus community has created. Further acknowledgements can be found in the NOTICE file