Spring Boot Scheduler
Many applications have work that must happen on its own: a report every morning, a clean-up every night, a check every few minutes. The Spring Boot scheduler runs such tasks automatically, at fixed intervals or at specific times, without any user action and without writing your own timer or thread code.
This tutorial covers enabling scheduling, the three ways to schedule a method, cron expressions, thread pools, error handling, dynamic schedules, running safely on several servers, and monitoring and testing your jobs.
1. What Is a Scheduler?
A scheduler starts a piece of code automatically according to a timetable. In Spring Boot, you write a normal method and add @Scheduled to it, and Spring calls the method at the right moments for as long as the application is running.
Think of the alarm on your phone. You set it once, for example “every weekday at 7:00”, and it goes off by itself. The @Scheduled annotation is the alarm setting, and your method is what happens when it rings.
2. Why Use a Scheduler?
Schedulers handle routine work that nobody should have to trigger by hand:
| Task | Typical schedule |
|---|---|
| Send a daily sales report by email | Every day at 08:00 |
| Delete expired password-reset tokens | Every hour |
| Cancel orders that were never paid | Every 15 minutes |
| Refresh exchange rates from an external API | Every 5 minutes |
| Remind members about overdue items | Weekdays at 09:00 |
| Archive old records | First day of every month |
3. How Scheduling Works in Spring Boot
When the application starts, Spring finds every method marked with @Scheduled and registers it with a task scheduler. The scheduler keeps a list of upcoming run times and starts each method on a scheduler thread when its time arrives.
An important detail: by default, Spring Boot’s scheduler has only one thread. All scheduled methods share it and run one at a time. If one task takes two minutes, every other task waits. Section 10 shows how to change this.
Scheduled methods must be in a Spring bean, should return void, and must not take any parameters, because nobody is there to pass them.
4. Enabling Scheduling
Scheduling is off by default. Switch it on with @EnableScheduling on a configuration class:
package com.javabytechie.shop.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
You can also place it on your @SpringBootApplication class. A separate class is handy because you can later switch scheduling off in tests by excluding just this class (see section 15).
5. Fixed Rate
fixedRate starts the method at a regular interval, measured from the start of one run to the start of the next:
@Component
public class ExchangeRateJob {
private static final Logger log = LoggerFactory.getLogger(ExchangeRateJob.class);
private final ExchangeRateService exchangeRateService;
public ExchangeRateJob(ExchangeRateService exchangeRateService) {
this.exchangeRateService = exchangeRateService;
}
// Starts every 5 minutes
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES)
public void refreshRates() {
exchangeRateService.refreshFromProvider();
log.info("Exchange rates refreshed");
}
}
If a run takes longer than the interval, the next run does not start alongside it. It starts as soon as the current one finishes. Runs of the same method never overlap unless you also make the method @Async.
Use it for: work that should happen at a steady pace, such as refreshing data or collecting measurements.
6. Fixed Delay
fixedDelay waits a set time after the previous run finishes before starting again:
@Component
public class UnpaidOrderJob {
private final OrderService orderService;
public UnpaidOrderJob(OrderService orderService) {
this.orderService = orderService;
}
// Waits 15 minutes after each run completes
@Scheduled(fixedDelay = 15, timeUnit = TimeUnit.MINUTES)
public void cancelUnpaidOrders() {
orderService.cancelOrdersUnpaidFor(Duration.ofHours(24));
}
}
Use it for: work whose duration varies, such as processing a queue, where you always want a quiet gap between runs.
| Task takes | fixedRate = 10 s: next start | fixedDelay = 10 s: next start |
|---|---|---|
| 2 seconds | 10 s after the previous start | 12 s after the previous start |
| 8 seconds | 10 s after the previous start | 18 s after the previous start |
| 15 seconds | Right after it finishes (it is late) | 25 s after the previous start |
7. Initial Delay and Time Units
initialDelay postpones the first run, giving the application time to warm up, connect to the database or load caches:
@Component
public class CacheRefreshJob {
// First run 1 minute after startup, then every 10 minutes
@Scheduled(initialDelay = 1, fixedRate = 10, timeUnit = TimeUnit.MINUTES)
public void refreshProductCache() {
// reload popular products into the cache
}
}
The timeUnit attribute keeps numbers readable. Without it, values are in milliseconds, so fixedRate = 600000 means 10 minutes, which is easy to misread.
| Attribute | Meaning |
|---|---|
fixedRate | Interval between the starts of runs |
fixedDelay | Pause between the end of one run and the start of the next |
initialDelay | Wait before the very first run |
timeUnit | Unit for the three values above (default: milliseconds) |
cron | Calendar-based schedule (next section) |
zone | Time zone used by a cron expression |
8. Cron Expressions
When a task should run at specific calendar times, such as “every weekday at 9:00”, use a cron expression. A Spring cron expression has six fields, separated by spaces:
┌───────────── second (0-59)
│ ┌─────────── minute (0-59)
│ │ ┌───────── hour (0-23)
│ │ │ ┌─────── day of month (1-31)
│ │ │ │ ┌───── month (1-12 or JAN-DEC)
│ │ │ │ │ ┌─── day of week (0-7 or MON-SUN; 0 and 7 are Sunday)
│ │ │ │ │ │
0 0 9 * * MON-FRI
@Component
public class DailyReportJob {
private final ReportService reportService;
public DailyReportJob(ReportService reportService) {
this.reportService = reportService;
}
// Every weekday at 09:00, Indian time
@Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Kolkata")
public void sendDailySalesReport() {
reportService.emailYesterdaysSales();
}
}
8.1 Special characters
| Character | Meaning | Example |
|---|---|---|
* | Every value | * in the hour field: every hour |
? | No specific value (day fields only) | 0 0 9 ? * MON |
- | A range | MON-FRI |
, | A list | 0 0 9,13,17 * * *: 9:00, 13:00 and 17:00 |
/ | Every n-th value | 0 */15 * * * *: every 15 minutes |
L | Last day of the month or week | 0 0 18 L * *: last day of each month at 18:00 |
# | The n-th weekday of the month | 0 0 10 * * MON#1: first Monday at 10:00 |
8.2 Common cron examples
| Cron expression | Runs |
|---|---|
0 * * * * * | Every minute |
0 0 * * * * | At the start of every hour |
0 */10 * * * * | Every 10 minutes |
0 0 0 * * * | Every day at midnight |
0 30 2 * * * | Every day at 02:30 |
0 0 9 * * MON-FRI | Weekdays at 09:00 |
0 0 8 * * SAT,SUN | Weekends at 08:00 |
0 0 0 1 * * | First day of every month at midnight |
0 0 6 1 1 * | Every 1 January at 06:00 |
Spring also understands shortcuts: @hourly, @daily, @weekly, @monthly and @yearly. Always set zone for cron jobs, so they do not depend on the server’s time zone.
9. Moving Schedules to Configuration
Hard-coded schedules require a new release to change. Put them in application.properties instead:
jobs.daily-report.cron=0 0 9 * * MON-FRI
jobs.daily-report.zone=Asia/Kolkata
jobs.cache-refresh.interval=PT10M
@Scheduled(cron = "${jobs.daily-report.cron}", zone = "${jobs.daily-report.zone}")
public void sendDailySalesReport() {
reportService.emailYesterdaysSales();
}
@Scheduled(fixedRateString = "${jobs.cache-refresh.interval}")
public void refreshProductCache() {
// ...
}
Now each environment can use its own schedule, for example every minute in testing and once a day in production. Interval strings such as PT10M use the ISO-8601 duration format: PT30S is 30 seconds, PT10M is 10 minutes and PT2H is 2 hours.
10. Thread Pool Configuration
Because the default scheduler has a single thread, one slow job delays all the others. Increase the pool size in application.properties:
spring.task.scheduling.pool.size=5
spring.task.scheduling.thread-name-prefix=scheduler-
spring.task.scheduling.shutdown.await-termination=true
spring.task.scheduling.shutdown.await-termination-period=30s
Or define the scheduler yourself, when you need more control:
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(30);
return scheduler;
}
}
A pool of 5 lets up to 5 different jobs run at the same time. Each individual job still never overlaps with itself. Thread names like scheduler-1 appear in your logs, which makes it easy to tell scheduled work apart from web requests.
11. Error Handling
If a scheduled method throws an exception, Spring logs it and the job keeps its schedule: the next run happens as normal. That is safe, but a log line is easy to miss. Catch errors yourself to add context and alerting:
@Scheduled(cron = "${jobs.daily-report.cron}", zone = "${jobs.daily-report.zone}")
public void sendDailySalesReport() {
long start = System.currentTimeMillis();
try {
reportService.emailYesterdaysSales();
log.info("Daily report sent in {} ms", System.currentTimeMillis() - start);
} catch (Exception ex) {
log.error("Daily report failed", ex);
alertService.notifyOnCall("Daily report failed: " + ex.getMessage());
}
}
To handle errors from all jobs in one place, give the scheduler an error handler:
@Bean
public ThreadPoolTaskScheduler taskScheduler(AlertService alertService) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(5);
scheduler.setThreadNamePrefix("scheduler-");
scheduler.setErrorHandler(ex -> {
LoggerFactory.getLogger("scheduler").error("Scheduled job failed", ex);
alertService.notifyOnCall("Scheduled job failed: " + ex.getMessage());
});
return scheduler;
}
12. Dynamic Schedules
Sometimes a schedule must change while the application is running, for example when an administrator edits it on a settings page. Implement SchedulingConfigurer and use a Trigger that reads the current schedule before every run:
@Configuration
@EnableScheduling
public class DynamicScheduleConfig implements SchedulingConfigurer {
private final JobSettingsRepository settings;
private final NewsletterService newsletterService;
public DynamicScheduleConfig(JobSettingsRepository settings, NewsletterService newsletterService) {
this.settings = settings;
this.newsletterService = newsletterService;
}
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
registrar.addTriggerTask(
newsletterService::sendWeeklyNewsletter,
triggerContext -> {
String cron = settings.findCron("newsletter"); // e.g. "0 0 10 * * MON"
return new CronTrigger(cron, ZoneId.of("Asia/Kolkata"))
.nextExecution(triggerContext);
});
}
}
Each time a run finishes, Spring asks the trigger for the next run time, so a changed cron value takes effect from the next run onwards, with no restart needed.
13. Running on Multiple Instances
If your application runs on three servers, every server runs every scheduled job. A daily report email is then sent three times. @Scheduled has no built-in protection against this, so you need a shared lock.
A popular open-source library for this is ShedLock. It records in a shared database table which server is running a job, so only one server runs it at a time. Add the dependencies:
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-spring</artifactId>
<version>${shedlock.version}</version>
</dependency>
<dependency>
<groupId>net.javacrumbs.shedlock</groupId>
<artifactId>shedlock-provider-jdbc-template</artifactId>
<version>${shedlock.version}</version>
</dependency>
Create the lock table in your database:
CREATE TABLE shedlock (
name VARCHAR(64) NOT NULL PRIMARY KEY,
lock_until TIMESTAMP NOT NULL,
locked_at TIMESTAMP NOT NULL,
locked_by VARCHAR(255) NOT NULL
);
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "PT30M")
public class SchedulerLockConfig {
@Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(new JdbcTemplate(dataSource))
.usingDbTime()
.build());
}
}
@Scheduled(cron = "${jobs.daily-report.cron}", zone = "${jobs.daily-report.zone}")
@SchedulerLock(name = "dailySalesReport", lockAtMostFor = "PT15M", lockAtLeastFor = "PT1M")
public void sendDailySalesReport() {
reportService.emailYesterdaysSales(); // runs on exactly one server
}
The other servers find the lock taken and skip that run. Alternatives include Quartz in clustered mode, or running jobs in a separate single-instance application.
14. Monitoring Scheduled Tasks
Spring Boot Actuator can list every scheduled task in your application, which is useful for checking that a schedule was picked up correctly:
management.endpoints.web.exposure.include=health,scheduledtasks
Then call GET /actuator/scheduledtasks:
{
"cron": [
{
"runnable": { "target": "com.javabytechie.shop.jobs.DailyReportJob.sendDailySalesReport" },
"expression": "0 0 9 * * MON-FRI"
}
],
"fixedDelay": [
{
"runnable": { "target": "com.javabytechie.shop.jobs.UnpaidOrderJob.cancelUnpaidOrders" },
"initialDelay": 0,
"interval": 900000
}
],
"fixedRate": [
{
"runnable": { "target": "com.javabytechie.shop.jobs.ExchangeRateJob.refreshRates" },
"initialDelay": 0,
"interval": 300000
}
]
}
Also log the start, end and duration of every important job, as in section 11. When something goes wrong at 3 a.m., these lines are the first place you will look.
15. Testing Scheduled Tasks
15.1 Test the logic directly
A scheduled method is just a method. The simplest test calls it directly and checks the result, without waiting for any timer:
class UnpaidOrderJobTest {
private final OrderService orderService = mock(OrderService.class);
private final UnpaidOrderJob job = new UnpaidOrderJob(orderService);
@Test
void cancelsOrdersUnpaidForOneDay() {
job.cancelUnpaidOrders();
verify(orderService).cancelOrdersUnpaidFor(Duration.ofHours(24));
}
}
15.2 Switch scheduling off in other tests
Jobs that fire during integration tests can change data in unexpected ways. Make the scheduling configuration depend on a property:
@Configuration
@EnableScheduling
@ConditionalOnProperty(name = "jobs.enabled", havingValue = "true", matchIfMissing = true)
public class SchedulingConfig {
}
jobs.enabled=false
Scheduling stays on everywhere else, and tests stay predictable.
16. Best Practices
- Keep the job class thin. Let the scheduled method call a service, so the real logic is easy to test and reuse.
- Make jobs safe to repeat. A job may run twice or stop halfway, so design it so that running again does no harm, for example “cancel orders that are still unpaid”.
- Process large data in batches instead of loading everything at once.
- Always set
zoneon cron jobs. - Move schedules to configuration, so they can change without a new release.
- Size the thread pool to the number of jobs that may run at the same time.
- Log start, end and duration, and alert on failures.
- Use a lock such as ShedLock when running more than one instance.
17. Common Mistakes to Avoid
| Mistake | What goes wrong | Fix |
|---|---|---|
Forgetting @EnableScheduling | Nothing runs, and there is no error | Add it to a configuration class |
| Using five cron fields, as in Linux | Startup fails with an invalid expression | Spring needs six fields, starting with seconds |
| Keeping the single default thread | One slow job delays all the others | Set spring.task.scheduling.pool.size |
| Running several instances without a lock | Jobs run once per server | Use ShedLock or a similar lock |
| Loading huge data sets in one go | Memory errors and long transactions | Process in batches |
| Writing intervals in raw milliseconds | 3600000 is easy to misread | Use timeUnit or PT1H |
| Ignoring the time zone | Jobs run at the wrong local time | Set zone on every cron job |
18. Summary
The Spring Boot scheduler turns any bean method into a background job with one annotation. Switch it on with @EnableScheduling, then choose fixedRate for a steady pace, fixedDelay for a gap between runs, or cron for calendar times. For production, move schedules into configuration, enlarge the thread pool, handle errors with alerts, add a lock when running several instances, and monitor your jobs with Actuator.
19. Frequently Asked Questions (FAQ)
Q1. Why is my @Scheduled method not running at all?
Work through this checklist:
@EnableSchedulingis missing. Without it,@Scheduledis silently ignored.- The class is not a Spring bean. It needs
@Component,@Serviceor similar, and must be inside a scanned package. - The method takes parameters. Scheduled methods must have none.
- A condition switched it off, such as
@Profileor@ConditionalOnPropertyon the class or configuration. - The schedule is further away than you think, for example a cron for 09:00 in another time zone.
GET /actuator/scheduledtasks quickly shows whether Spring registered the job.
Q2. Why do my scheduled jobs run late or one after another?
Spring Boot’s scheduler uses one thread by default. While one job runs, every other job that is due must wait for it to finish.
Give the scheduler more threads:
spring.task.scheduling.pool.size=5
Check the thread names in your logs. If every job shows scheduling-1, they are all sharing a single thread.
Q3. Can two runs of the same job overlap?
Not by default. Spring never starts a job again while its previous run is still going, even with fixedRate and a larger thread pool. A late run simply starts as soon as the previous one ends.
Overlap only happens if you also mark the method @Async, which hands each run to a separate executor:
@Async
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
public void pollSensors() {
// runs may now overlap if one takes longer than a minute
}
Only do this when the job is safe to run in parallel with itself.
Q4. How do I run a task only once when the application starts?
@Scheduled is for repeated work. For a one-time task at startup, use an ApplicationRunner, or listen for the ApplicationReadyEvent:
@Component
public class StartupTasks {
@EventListener(ApplicationReadyEvent.class)
public void loadReferenceData() {
// runs once, after the application is fully started
}
}
ApplicationReadyEvent fires after the application is completely ready, which makes it a safe moment to call other services.
Q5. Why does my cron job run at the wrong hour?
Without a zone, Spring uses the server’s time zone. Cloud servers and containers often run in UTC, so 0 0 9 * * * means 09:00 UTC, which is 14:30 in India.
Always state the zone you mean:
@Scheduled(cron = "0 0 9 * * *", zone = "Asia/Kolkata")
Use region names like Asia/Kolkata or Europe/London instead of fixed offsets like +05:30, so daylight saving time is handled correctly in regions that use it.
Q6. How does daylight saving time affect cron jobs?
In regions with daylight saving time, clocks jump forward one night and back another. A job scheduled inside that hour may be skipped on the spring-forward night or seem to happen at an unexpected moment on the fall-back night.
Simple ways to stay safe:
- Avoid scheduling important jobs between about 01:00 and 03:00 local time in such regions.
- Or run the job in
zone = "UTC", which has no daylight saving changes. - Make jobs safe to repeat, so an extra run does no harm.
Regions without daylight saving, such as India, are not affected.
Q7. What is the difference between * and ? in a cron expression?
*means every value of the field.?means no specific value, and only applies to the day-of-month and day-of-week fields.
Use ? to show that one day field is not relevant because the other one decides the day:
0 0 9 ? * MON every Monday at 09:00 (day of month does not matter)
0 0 9 15 * ? the 15th of every month at 09:00 (weekday does not matter)
In Spring, 0 0 9 * * MON works the same way, so ? is mostly about readability and compatibility with other tools such as Quartz.
Q8. How do I schedule a job for the last day or first Monday of the month?
Spring cron supports special characters for these cases:
| Expression | Runs |
|---|---|
0 0 18 L * * | Last day of every month at 18:00 |
0 0 18 L-2 * * | Two days before the end of the month |
0 0 9 LW * * | Last weekday (Mon–Fri) of the month |
0 0 10 * * MON#1 | First Monday of the month |
0 0 10 * * FRI#3 | Third Friday of the month |
0 0 17 * * FRIL | Last Friday of the month |
L handles months of different lengths and leap years automatically.
Q9. How do I switch off a scheduled job without changing the code?
If the cron expression comes from a property, set it to a single dash. Spring treats "-" as “disabled”:
jobs.daily-report.cron=-
For fixedRate or fixedDelay jobs, put @ConditionalOnProperty on the job class so the bean is only created when enabled:
@Component
@ConditionalOnProperty(name = "jobs.exchange-rates.enabled", havingValue = "true")
public class ExchangeRateJob { ... }
Q10. How do I pause and resume a job while the application is running?
A simple approach is a switch that the job checks before doing any work:
@Component
public class NewsletterJob {
private final AtomicBoolean paused = new AtomicBoolean(false);
@Scheduled(cron = "0 0 10 * * MON", zone = "Asia/Kolkata")
public void send() {
if (paused.get()) {
return; // skip this run
}
// send the newsletter
}
public void pause() { paused.set(true); }
public void resume() { paused.set(false); }
}
Expose pause() and resume() through a secured admin endpoint. With several instances, store the flag in a database or shared cache so every server sees the same value.
Q11. What do lockAtMostFor and lockAtLeastFor mean in ShedLock?
| Setting | Meaning | Why it matters |
|---|---|---|
lockAtMostFor | The lock is released after this time even if the job has not finished | If a server crashes mid-run, the job is not blocked forever |
lockAtLeastFor | The lock is held for at least this long, even if the job finishes sooner | Stops another server with a slightly different clock from running the same job again |
Set lockAtMostFor clearly longer than the job’s normal duration. If it is too short, the lock can expire while the job is still running and a second server may start it.
Q12. Should I use @Scheduled or Quartz?
@Scheduled | Quartz | |
|---|---|---|
| Setup | One annotation | Library, configuration and usually database tables |
| Schedules stored | In code or properties | In a database; can be added and changed at runtime |
| Missed runs (app was down) | Skipped | Can be caught up with “misfire” rules |
| Clustering | Needs an extra lock such as ShedLock | Built in |
Start with @Scheduled. Move to Quartz when you need many jobs that users create and edit, or must not miss a run while the application is down. Spring Boot has a spring-boot-starter-quartz starter for this.
Q13. When should I use Spring Batch instead of a plain scheduled job?
A scheduled job decides when work runs. Spring Batch helps with how to process large amounts of data reliably. Consider Spring Batch when a job:
- Processes thousands or millions of records, such as importing a large file.
- Must restart from where it stopped after a failure, instead of starting over.
- Needs a record of every run: start time, end time, records read, written and skipped.
The two work together: a @Scheduled method can launch a Spring Batch job at the right time.
Q14. Should a scheduled method be @Transactional?
Put @Transactional on the service method the job calls, not on the scheduled method itself. The job class stays a thin trigger, and the service controls its own database work.
Keep transactions short. One giant transaction around a job that updates 100,000 rows holds database locks for a long time and is fully rolled back if the last row fails. Process the work in smaller batches, each in its own transaction:
@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Kolkata")
public void archiveOldOrders() {
int archived;
do {
archived = orderArchiveService.archiveNextBatch(500); // @Transactional, 500 rows
} while (archived > 0);
}
Q15. What happens if the application is down when a job should run?
The run is simply missed. @Scheduled keeps no history, so after a restart it waits for the next scheduled time and does not catch up.
If a missed run matters, for example a daily billing job, design for it:
- Save the time of the last successful run in the database, and at startup check whether a run was missed.
- Make the job process everything not yet done, such as all unbilled orders, instead of “today’s orders”. Then the next run catches up automatically.
- Or use Quartz, which has built-in rules for missed runs.
Q16. Can scheduled jobs use virtual threads?
Yes. On Java 21 with Spring Boot 3.2 or later, enable virtual threads:
spring.threads.virtual.enabled=true
Spring Boot then runs each scheduled job on its own virtual thread, so jobs no longer wait for a free thread and spring.task.scheduling.pool.size no longer applies. Each job still does not overlap with itself.
This suits jobs that mostly wait on databases or remote calls. Keep an eye on what they call: many jobs starting at once can still overload a database.
Q17. How do I stop a job that runs much longer than expected?
@Scheduled has no built-in time limit. Protect your jobs with timeouts at each slow step, and a time budget for the job as a whole:
@Scheduled(fixedDelay = 10, timeUnit = TimeUnit.MINUTES)
public void syncProducts() {
Instant deadline = Instant.now().plus(Duration.ofMinutes(8));
for (Long id : productService.idsToSync()) {
if (Instant.now().isAfter(deadline)) {
log.warn("Sync stopped at time budget; the rest continues next run");
return;
}
productService.syncOne(id); // uses an HTTP client with its own timeouts
}
}
Also set connection and read timeouts on HTTP clients and query timeouts on the database, so a single slow call cannot block the job forever.
Q18. What rules must a @Scheduled method follow?
- Visibility: make it
public. This is the safe choice on every Spring version, and it is required when you combine the method with proxy-based features such as@Asyncor@Transactional. - Return values: anything returned is ignored, because nobody receives it. Use
void. - Parameters: not allowed. Startup fails if a scheduled method has any.
- Static methods: not supported. The method must belong to a bean instance.
Q19. How do I measure how long each scheduled job takes?
Since Spring Framework 6.1 (Spring Boot 3.2), scheduled methods are measured automatically when Actuator and Micrometer are present. Each job records a timer named tasks.scheduled.execution, tagged with the class and method:
GET /actuator/metrics/tasks.scheduled.execution
The timer shows how many times the job ran, its total and maximum duration, and whether runs succeeded or failed. Export it to a monitoring system such as Prometheus, and add an alert when a job is slower than usual or has not succeeded for a while.
Q20. Should I use @Scheduled for work triggered by users?
Usually not. A common anti-pattern is a job that checks a table every few seconds for new work, such as “emails to send”. It adds a delay, keeps querying the database even when there is nothing to do, and needs locking when several instances run.
Better options for work started by a user action:
@Asyncto run the work in the background right away.- A message broker such as Kafka or RabbitMQ when the work must not be lost.
- Application events with
@TransactionalEventListenerto react after a save.
Keep @Scheduled for work that is truly driven by time.