Scheduling with @Scheduled
Intermediate@Scheduled triggers a method on a fixed rate, fixed delay, or cron expression; @EnableScheduling must be present and methods must return void.
Overview
Spring's `@Scheduled` annotation turns any `void` Spring bean method into a background task. `@EnableScheduling` (on a `@Configuration` class or the main class) activates the scheduler. Three trigger modes: **fixedRate** (fire every N ms regardless of execution time — can overlap if the task takes longer), **fixedDelay** (wait N ms after the previous execution completes — no overlap), and **cron** (UNIX-style 6-field cron expression). By default all `@Scheduled` tasks share a single-threaded `ThreadPoolTaskScheduler` — tasks queue behind each other. For concurrent tasks, configure a `TaskScheduler` bean with a larger pool size. For clustered deployments, use ShedLock or Quartz to prevent duplicate execution across pods.
fixedRate, fixedDelay, and cron
`fixedRate` fires at a fixed interval from the previous **start** time — runs overlap if execution exceeds the period. `fixedDelay` fires N ms after the previous **end** time — safe for tasks that must not overlap. `cron` is most flexible for calendar-based schedules. Use `initialDelay` to delay the first run after startup.
@Configuration
@EnableScheduling
public class SchedulingConfig { }
@Component
public class ReportScheduler {
// Every 30 seconds regardless of execution time (can overlap)
@Scheduled(fixedRate = 30_000)
public void generateMetricsSnapshot() {
metricsService.snapshot();
}
// 10 seconds AFTER the last run completes (no overlap)
@Scheduled(fixedDelay = 10_000, initialDelay = 5_000)
public void cleanExpiredSessions() {
sessionRepo.deleteExpired(Instant.now());
}
// CRON: every day at 02:30 (6-field: sec min hour day month weekday)
@Scheduled(cron = "0 30 2 * * *")
public void dailyEmailReport() {
reportService.sendDaily();
}
// Read cron from config — allows per-environment schedule
@Scheduled(cron = "${reports.invoice.cron:0 0 1 * * *}")
public void invoiceRun() {
invoiceService.processAll();
}
}Custom TaskScheduler for Concurrent Tasks
The default scheduler is single-threaded — a long-running task blocks all others. Provide a `TaskScheduler` bean with a named thread pool. Use `ThreadPoolTaskScheduler` for multiple tasks running concurrently, or `@Async` on individual tasks to offload them from the scheduler thread.
@Configuration
@EnableScheduling
public class SchedulingConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5); // 5 concurrent scheduled tasks
scheduler.setThreadNamePrefix("sched-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30); // graceful shutdown
return scheduler;
}
}
// Alternative: @Async to offload a specific task from the scheduler thread
@Scheduled(fixedDelay = 5_000)
@Async("taskExecutor") // runs in a separate thread pool
public void heavyDataSync() {
// Long-running — won't block other @Scheduled tasks
dataSync.syncAll();
}Distributed Scheduling with ShedLock
In a multi-pod Kubernetes deployment, every pod runs its own scheduler — `@Scheduled` tasks execute on all pods simultaneously. **ShedLock** uses a distributed lock (stored in a DB table, Redis, or Mongo) to ensure only one pod executes each task at a time. Annotate the method with `@SchedulerLock(name = "taskName", lockAtMostFor = "PT10M")`.
<!-- pom.xml -->
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>5.13.0</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>5.13.0</version>
</dependency>
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT5M")
public class ShedLockConfig {
@Bean
public LockProvider lockProvider(DataSource ds) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(ds))
.usingDbTime()
.build());
}
}
@Scheduled(cron = "0 0 2 * * *")
@SchedulerLock(name = "dailyReport",
lockAtMostFor = "PT30M", // release lock after 30min even if node dies
lockAtLeastFor = "PT1M") // hold lock for min 1min (avoid double-fire on restart)
public void dailyReport() {
reportService.generate();
}Key Points to Remember
- 1fixedRate: fires every N ms from last start — can overlap; fixedDelay: N ms after last end — safe
- 2cron uses 6 fields (sec min hour day month weekday) — externalise via ${} for per-env config
- 3Default scheduler is single-threaded — provide a ThreadPoolTaskScheduler bean for concurrency
- 4@Async on a @Scheduled method offloads it to a separate executor thread pool
- 5Multi-pod deployments: all pods run @Scheduled independently — use ShedLock for one-pod-only
- 6setWaitForTasksToCompleteOnShutdown(true) ensures graceful drain on SIGTERM
Interview Questions
Sign in to ask AriaWhat is the difference between fixedRate and fixedDelay in @Scheduled?
Why does the default @Scheduled setup cause problems when you have multiple tasks?
How would you ensure a scheduled task runs on only one pod in a Kubernetes deployment?
How would you make a @Scheduled task's schedule configurable per environment?
What is ShedLock and how does it prevent duplicate scheduled task execution?
Ask Aria about Scheduling with @Scheduled
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.