Spring: Task Scheduling with @Scheduled

Spring's scheduling support lets you run methods on a fixed schedule (every N milliseconds, after a delay, or using a cron expression) without adding a third-party scheduler library. All you need is @EnableScheduling and @Scheduled.

Contents

@EnableScheduling

Add @EnableScheduling to a @Configuration class (or directly on @SpringBootApplication).

Java
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {
    // Optionally define a TaskScheduler bean here
}
By default Spring uses a single-threaded scheduler. If one task is running, the next scheduled invocation waits. Configure a ThreadPoolTaskScheduler to run tasks concurrently.

@Scheduled with fixedRate / fixedDelay / cron

@Scheduled runs a method at a fixed rate, with a fixed delay between runs, or on a cron schedule.

Java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ReportScheduler {

    // fixedRate β€” run every 5 seconds measured from the START of the previous execution
    @Scheduled(fixedRate = 5000)
    public void generateReport() {
        System.out.println("Report generated at " + Instant.now());
    }

    // fixedDelay β€” wait 5 seconds AFTER the previous execution FINISHES before running again
    @Scheduled(fixedDelay = 5000)
    public void cleanUpTempFiles() {
        System.out.println("Cleanup done at " + Instant.now());
    }

    // cron β€” run at 2:30 AM every day
    @Scheduled(cron = "0 30 2 * * ?")
    public void dailyBackup() {
        System.out.println("Daily backup started at " + Instant.now());
    }

    // Use ISO duration strings (Spring 5.1+) β€” cleaner than raw ms
    @Scheduled(fixedRateString = "PT10S")   // every 10 seconds
    public void pingHealthCheck() { }

    @Scheduled(fixedDelayString = "PT1M")   // 1 minute after last run
    public void syncData() { }

    // Read from application.properties
    @Scheduled(cron = "${reports.schedule.cron:0 0 6 * * ?}")
    public void configuredJob() { }
}
ModeTiming measured fromOverlap risk
fixedRateStart of previous executionYes: if task takes longer than the rate
fixedDelayEnd of previous executionNo: always waits for previous to finish
cronCalendar timeNo (single thread): can pile up with thread pool

Cron Expression Syntax

Spring uses a 6-field cron expression: second minute hour day-of-month month day-of-week.

FieldAllowed valuesSpecial chars
Second0–59, - * /
Minute0–59, - * /
Hour0–23, - * /
Day of month1–31, - * ? / L W
Month1–12 or JAN–DEC, - * /
Day of week0–7 (0,7=Sun) or SUN–SAT, - * ? / L #
Java
// Every minute
@Scheduled(cron = "0 * * * * ?")

// Every day at midnight
@Scheduled(cron = "0 0 0 * * ?")

// Every weekday at 9 AM
@Scheduled(cron = "0 0 9 * * MON-FRI")

// Every 30 minutes
@Scheduled(cron = "0 0/30 * * * ?")

// First day of every month at 6 AM
@Scheduled(cron = "0 0 6 1 * ?")

// Every Sunday at 3 AM
@Scheduled(cron = "0 0 3 ? * SUN")

@Scheduled with initialDelay

initialDelay delays the first execution after application startup. Useful to avoid running tasks before the application is fully ready.

Java
@Component
public class WarmupTask {

    // Wait 30 seconds after startup, then run every 10 minutes
    @Scheduled(initialDelay = 30_000, fixedRate = 600_000)
    public void refreshCache() {
        cacheService.refresh();
    }

    // ISO duration strings
    @Scheduled(initialDelayString = "PT30S", fixedRateString = "PT10M")
    public void pollExternalApi() {
        apiClient.poll();
    }

    // From properties
    @Scheduled(initialDelayString = "${scheduler.initial-delay:PT1M}", fixedDelayString = "PT5M")
    public void syncData() {
        dataSync.run();
    }
}

Dynamic Scheduling with TaskScheduler

For schedules that are determined at runtime (e.g., from a database), inject a TaskScheduler and schedule tasks programmatically.

Java
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import java.util.concurrent.ScheduledFuture;

@Service
public class DynamicSchedulerService {

    private final TaskScheduler taskScheduler;
    private ScheduledFuture<?> scheduledTask;

    public DynamicSchedulerService(TaskScheduler taskScheduler) {
        this.taskScheduler = taskScheduler;
    }

    public void scheduleTask(String cronExpression) {
        // Cancel existing task if running
        if (scheduledTask != null) {
            scheduledTask.cancel(false);
        }
        scheduledTask = taskScheduler.schedule(
            this::doWork,
            new CronTrigger(cronExpression)
        );
    }

    public void scheduleFixedRate(long intervalMs) {
        if (scheduledTask != null) {
            scheduledTask.cancel(false);
        }
        scheduledTask = taskScheduler.scheduleAtFixedRate(
            this::doWork,
            Duration.ofMillis(intervalMs)
        );
    }

    public void cancelTask() {
        if (scheduledTask != null) {
            scheduledTask.cancel(false);
        }
    }

    private void doWork() {
        System.out.println("Dynamic task executed at " + Instant.now());
    }
}

ThreadPoolTaskScheduler

The default single-threaded scheduler blocks concurrent task execution. Configure a ThreadPoolTaskScheduler to allow tasks to run in parallel.

Java
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableScheduling
public class SchedulingConfig {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);                           // 5 concurrent tasks
        scheduler.setThreadNamePrefix("scheduled-task-");
        scheduler.setAwaitTerminationSeconds(60);
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        return scheduler;
    }
}
With a thread pool, multiple scheduled tasks can run concurrently. Ensure your task methods are thread-safe, especially if they share mutable state. Use fixedDelay instead of fixedRate if overlap is a concern.