Scheduling Tasks
Many applications need work to happen on a timer rather than in response to a request: purging expired sessions every night, polling an inbox every minute, refreshing a cache every few seconds. Spring Boot’s scheduling support lets you turn any bean method into a recurring job with a single annotation — no external scheduler, no cron daemon, no quartz configuration for the common cases.
Enabling scheduling
Switch on the feature with @EnableScheduling, usually on the main class or a config class. Without it, @Scheduled annotations are silently ignored.
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
@Scheduled triggers
Annotate a no-argument method on a Spring-managed bean. There are three ways to describe when it runs.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class ReportJobs {
private static final Logger log = LoggerFactory.getLogger(ReportJobs.class);
// Runs every 5 seconds, measured from the START of the previous run
@Scheduled(fixedRate = 5000)
public void pollQueue() {
log.info("Polling queue at {}", System.currentTimeMillis());
}
// Runs 10 seconds after the PREVIOUS run FINISHES (no overlap)
@Scheduled(fixedDelay = 10_000, initialDelay = 2000)
public void syncInventory() {
log.info("Syncing inventory");
}
// Runs at 02:30 every day (second minute hour day month weekday)
@Scheduled(cron = "0 30 2 * * *", zone = "Europe/London")
public void nightlyReport() {
log.info("Generating nightly report");
}
}
Output (console):
2026-06-13T02:30:00.004 INFO ReportJobs : Generating nightly report
2026-06-13T10:15:02.011 INFO ReportJobs : Syncing inventory
2026-06-13T10:15:05.001 INFO ReportJobs : Polling queue at 1781000105001
| Trigger | Counts from | Use when |
|---|---|---|
fixedRate | start of previous run | you want a steady cadence regardless of duration |
fixedDelay | end of previous run | runs must never overlap; spacing matters |
cron | wall-clock expression | calendar schedules (daily, weekday, monthly) |
Tip: With
fixedRate, if a run takes longer than the interval the next run waits its turn (the default pool has one thread) — it does not run concurrently. PreferfixedDelaywhenever overlap would cause problems.
Cron expressions
Spring uses a 6-field cron format: second minute hour day-of-month month day-of-week. This differs from the classic 5-field Unix cron.
| Expression | Meaning |
|---|---|
0 0 * * * * | top of every hour |
0 */15 * * * * | every 15 minutes |
0 0 9-17 * * MON-FRI | hourly, 9am–5pm, weekdays |
0 0 0 1 * * | midnight on the 1st of every month |
@daily | midnight every day (macro) |
Externalize the expression so ops can tune it without a rebuild:
app:
jobs:
report-cron: "0 30 2 * * *"
@Scheduled(cron = "${app.jobs.report-cron}")
public void nightlyReport() { ... }
Note: Setting
cronto the special value"-"disables a scheduled method entirely — handy for switching a job off via configuration in certain environments.
The scheduler thread pool
By default Spring uses a single-threaded scheduler, so all @Scheduled methods share one thread and a long job blocks the others. For multiple independent jobs, size the pool explicitly.
spring:
task:
scheduling:
pool:
size: 4
thread-name-prefix: scheduling-
Or define a TaskScheduler bean for finer control:
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Bean
TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(4);
scheduler.setThreadNamePrefix("scheduling-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
Conditional scheduling
Often a job should run only in certain environments — for example, the nightly cleanup should run in production but not on every developer’s laptop. Gate the whole bean with @ConditionalOnProperty or a profile.
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@Component
@ConditionalOnProperty(name = "app.jobs.enabled", havingValue = "true")
public class ReportJobs { ... }
# only in the production profile
app:
jobs:
enabled: true
The distributed scheduling caveat
@Scheduled runs independently in every instance. If you deploy three replicas, a fixedRate job fires three times per interval, and a nightly report generates three times. For a job that must run exactly once across the cluster, you need coordination:
- ShedLock (
net.javacrumbs.shedlock) — a lightweight library that takes a lock in a shared store (database, Redis) so only one node executes a given run. Annotate with@SchedulerLock. - A dedicated distributed scheduler (Quartz with a clustered JobStore, or an external orchestrator like Kubernetes CronJob).
Warning: Never assume
@Scheduledfires once in a multi-instance deployment. Audit every scheduled method and add locking (ShedLock) for any job that has side effects like sending emails, charging cards, or writing reports.
Best Practices
- Pick
fixedDelaywhen overlap is harmful,fixedRatefor steady cadence,cronfor calendar times. - Size the scheduler pool to the number of independent jobs; the default is one thread.
- Externalize cron expressions and gate jobs with
@ConditionalOnProperty/profiles. - Use ShedLock (or equivalent) for jobs that must run exactly once across instances.
- Keep scheduled methods short or offload heavy work to an
@Asyncexecutor so the scheduler thread stays free.