Spring Boot Scheduler Tutorial
Watch on YouTubeSpring Boot Scheduler
Spring Boot scheduling is useful for cleanup jobs, notifications, report generation, data synchronization, cache refreshes, and periodic health checks. The scheduler should coordinate small, observable units of work; long-running or distributed workflows may need a queue, batch platform, or Quartz.
Enable scheduling
Add @EnableScheduling to a configuration or application class. Spring then detects @Scheduled methods on beans.
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
A scheduled method must be on a Spring-managed bean. Keep its signature simple: it should not require arguments, and its return value is not used by the scheduler.
Create a scheduled task
import java.time.Instant;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class OrderCleanupJob {
@Scheduled(fixedDelay = 10_000)
public void removeExpiredOrders() {
System.out.println("Cleanup started at " + Instant.now());
}
}
By default, scheduling is triggered by the Spring application context. Do not create the job with new; otherwise Spring cannot apply its scheduling infrastructure.
Fixed rate versus fixed delay
| Trigger | Meaning | Use when |
|---|---|---|
fixedRate | Starts on a regular interval measured from the scheduled start time. | Runs should follow a regular cadence. |
fixedDelay | Waits for the previous invocation to finish, then waits the configured delay. | The next run must wait until the previous run completes. |
initialDelay | Waits before the first invocation. | The application needs time to finish startup. |
@Scheduled(fixedRate = 60_000, initialDelay = 10_000)
public void refreshCache() { }
@Scheduled(fixedDelay = 5_000)
public void publishPendingEvents() { }
Use java.util.concurrent.TimeUnit to make units explicit:
import java.util.concurrent.TimeUnit;
@Scheduled(fixedDelay = 30, timeUnit = TimeUnit.SECONDS)
public void checkPendingPayments() { }
Use cron expressions
Use cron when a job must run at a calendar time. Spring cron expressions use six fields: second, minute, hour, day of month, month, and day of week.
@Scheduled(cron = "0 0 2 * * *")
public void createDailyReport() { }
@Scheduled(cron = "0 */15 * * * *")
public void syncEveryFifteenMinutes() { }
@Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Kolkata")
public void sendWeekdayReminder() { }
| Expression | Schedule |
|---|---|
0 0 2 * * * | Every day at 02:00:00. |
0 */15 * * * * | Every 15 minutes. |
0 0 9 * * MON-FRI | Weekdays at 09:00 in the configured time zone. |
Externalize expressions when operations teams need to change the schedule without editing Java code:
orders.cleanup.cron=0 0 2 * * *
@Scheduled(cron = "${orders.cleanup.cron}", zone = "UTC")
public void removeExpiredOrders() { }
Configure a scheduler thread pool
A production application should make scheduler capacity explicit. Configure a pool size when jobs may overlap or when several jobs must run independently.
spring.task.scheduling.pool.size=4
spring.task.scheduling.thread-name-prefix=orders-scheduler-
spring.task.scheduling.shutdown.await-termination=true
spring.task.scheduling.shutdown.await-termination-period=30s
Keep the pool size proportional to the work. More threads do not make a database or downstream API faster; they can increase contention. Measure execution time, queueing, failures, and downstream capacity.
Define a custom scheduler when needed
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class SchedulingConfig {
@Bean
TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(4);
scheduler.setThreadNamePrefix("orders-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
return scheduler;
}
}
Prevent overlapping and duplicate work
Each application instance can execute its own scheduled task. In a multi-instance deployment, the same job may run more than once at the same time. Choose a strategy based on the job:
- Make the operation idempotent so repeating it is safe.
- Use a database lock, unique key, or lease for short critical sections.
- Use a distributed scheduler or a library such as ShedLock when one cluster-wide execution is required.
- Use a queue or batch system for long-running, retryable, or high-volume work.
- Use Quartz when you need persistent triggers, calendars, misfire handling, or richer job management.
Do not rely on an in-memory scheduler alone for a business-critical job whose execution history must survive a restart.
Handle failures and observe jobs
Scheduled exceptions should be visible in logs and metrics. Catch only exceptions you can handle, preserve the cause, and let monitoring alert on repeated failures.
@Scheduled(cron = "${orders.sync.cron}")
public void synchronizeOrders() {
try {
orderSyncService.sync();
} catch (TransientSyncException ex) {
log.warn("Order synchronization will be retried", ex);
}
}
For production, record job name, start time, duration, result, item count, and failure reason. Avoid logging passwords, tokens, full request bodies, or sensitive customer data.
Test scheduled services
Keep scheduling separate from business logic so most tests can call the service directly. Use a test profile or property to disable automatic scheduling during tests.
# application-test.properties
spring.task.scheduling.enabled=false
@SpringBootTest
@ActiveProfiles("test")
class OrderCleanupJobTest {
@Autowired
private OrderCleanupJob job;
@Test
void removesExpiredOrdersWhenJobRuns() {
job.removeExpiredOrders();
// Assert the service or repository interaction.
}
}
Use integration tests for scheduler wiring and focused unit tests for the work performed by each job. Avoid tests that wait for real-time cron intervals.
Spring Boot Scheduler best practices
- Keep scheduled methods small and delegate business work to a service.
- Choose
fixedDelay,fixedRate, or cron based on the business requirement. - Specify a time zone for calendar-sensitive jobs.
- Externalize schedules so operations can change them safely.
- Configure thread-pool capacity and graceful shutdown.
- Make jobs idempotent and safe to retry.
- Protect multi-instance jobs with a distributed coordination strategy.
- Measure duration, success, failure, and skipped work.
- Use a queue, Spring Batch, or Quartz when simple in-memory scheduling is not enough.
Spring Boot Scheduler FAQs
What annotation enables scheduled tasks?
Add @EnableScheduling to a configuration class, then place @Scheduled on a method in a Spring-managed bean.
What is the difference between fixed rate and fixed delay?
fixedRate follows a regular start-time cadence, while fixedDelay waits for the previous invocation to finish before starting the delay.
Can a scheduled task run twice in a cluster?
Yes. Each application instance has its own scheduler unless you add distributed coordination. Make work idempotent or use a lock, queue, or persistent scheduler.
When should I use Quartz instead of @Scheduled?
Use Quartz when you need persistent jobs, calendars, misfire policies, richer trigger management, or durable execution history.
Continue learning
