Back to Blog
Java12 min read

Java Scheduled Tasks: Spring @Scheduled, Quartz & Timer Guide

Java offers multiple approaches to scheduled task execution — from Spring's simple @Scheduled annotation to the enterprise-grade Quartz Scheduler and built-in JDK utilities. This guide covers each approach with practical code examples, explains Spring's 6-field cron expression syntax, and shows how to monitor your Java scheduled tasks in production.

Enabling Scheduling in Spring Boot

Spring Boot does not enable task scheduling by default. You need to add the @EnableScheduling annotation to any @Configuration class (or your main application class):

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

This single annotation registers a TaskScheduler bean and starts scanning for @Scheduled methods in all Spring-managed beans. By default, scheduled tasks run on a single thread, which means only one task executes at a time.

The @Scheduled Annotation

The @Scheduled annotation supports three scheduling strategies: fixedRate, fixedDelay, and cron. Each serves a different use case.

fixedRate: Constant Interval from Start

Executes at a fixed interval measured from the start of each invocation. If the previous execution is still running, the next one waits (on the default single-thread pool):

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

@Component
public class PollingService {

    // Run every 5 seconds (5000 milliseconds)
    @Scheduled(fixedRate = 5000)
    public void pollExternalApi() {
        // Fetch latest data from external API
        log.info("Polling external API...");
        apiClient.fetchUpdates();
    }

    // With an initial delay of 10 seconds before the first run
    @Scheduled(fixedRate = 30000, initialDelay = 10000)
    public void syncInventory() {
        log.info("Syncing inventory...");
        inventoryService.sync();
    }
}

fixedDelay: Gap After Completion

Waits for the specified duration after each execution completes before starting the next one. This guarantees a minimum gap between runs:

@Component
public class BatchProcessor {

    // Wait 10 seconds after each completion before starting the next run
    @Scheduled(fixedDelay = 10000)
    public void processBatch() {
        List<Order> orders = orderRepository.findPending();
        for (Order order : orders) {
            orderService.process(order);
        }
        // After this method returns, Spring waits 10 seconds, then calls again
    }
}

Tip

You can use string-based expressions for externalized configuration: @Scheduled(fixedRateString = "\${polling.interval}"). This lets you change the interval via application.properties without recompiling.

cron: Cron Expression Scheduling

For complex schedules, use a cron expression. Spring uses a 6-field format that differs from standard Unix cron:

@Component
public class ReportService {

    // Every day at 2:00 AM (note: 6 fields, starting with seconds)
    @Scheduled(cron = "0 0 2 * * *")
    public void generateDailyReport() {
        reportGenerator.createDailyReport();
    }

    // Every Monday at 9:00 AM
    @Scheduled(cron = "0 0 9 * * MON")
    public void sendWeeklyDigest() {
        emailService.sendWeeklyDigest();
    }

    // Every 15 minutes during business hours (9 AM - 5 PM), weekdays only
    @Scheduled(cron = "0 */15 9-17 * * MON-FRI")
    public void checkAlerts() {
        alertService.checkAndNotify();
    }

    // With timezone
    @Scheduled(cron = "0 0 9 * * MON-FRI", zone = "America/New_York")
    public void businessHoursTask() {
        // Runs at 9 AM Eastern time, regardless of server timezone
    }
}

Spring Cron Expression Syntax (6 Fields vs 5)

This is the most common source of confusion. Standard Unix cron uses 5 fields. Spring adds a seconds field at the beginning, giving you 6 fields. Refer to the cron expression cheatsheet for the standard 5-field format.

FieldPositionAllowed ValuesExample
Second1st0-590
Minute2nd0-590, */15
Hour3rd0-232, 9-17
Day of Month4th1-311, 15
Month5th1-12 or JAN-DEC*, 1-6
Day of Week6th0-7 or MON-SUNMON-FRI, *

Common Spring Cron Expressions

Spring ExpressionUnix EquivalentDescription
0 0 * * * *0 * * * *Every hour
0 0 2 * * *0 2 * * *Daily at 2:00 AM
0 */30 * * * **/30 * * * *Every 30 minutes
0 0 9 * * MON-FRI0 9 * * 1-5Weekdays at 9:00 AM
0 0 0 1 * *0 0 1 * *First day of every month at midnight

Important

A 5-field Unix cron expression like 0 2 * * * will cause a parsing error in Spring. Always include the seconds field. Use a cron expression generator to avoid syntax mistakes.

Configuring the Thread Pool

By default, Spring uses a single thread for all scheduled tasks. If one task takes 30 seconds, all other tasks are blocked during that time. Configure a thread pool to run tasks concurrently:

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);  // 5 concurrent scheduled tasks
        scheduler.setThreadNamePrefix("scheduled-");
        scheduler.setErrorHandler(t -> {
            log.error("Scheduled task failed", t);
            // Send alert to monitoring
        });
        scheduler.initialize();
        registrar.setTaskScheduler(scheduler);
    }
}

Alternatively, in Spring Boot 2.1+, you can configure the pool size in application.properties:

# application.properties
spring.task.scheduling.pool.size=5
spring.task.scheduling.thread-name-prefix=scheduled-

Quartz Scheduler Overview

Quartz Scheduler is the enterprise-grade option for Java task scheduling. It provides features that @Scheduled does not: persistent job storage, clustering, misfire handling, and runtime job management.

Spring Boot + Quartz Setup

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
// Define a Quartz Job
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.stereotype.Component;

@Component
public class ReportJob implements Job {

    @Override
    public void execute(JobExecutionContext context) {
        String reportType = context.getMergedJobDataMap().getString("reportType");
        // Generate the report
        reportService.generate(reportType);
    }
}
// Configure the Job and Trigger
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail reportJobDetail() {
        return JobBuilder.newJob(ReportJob.class)
            .withIdentity("reportJob")
            .usingJobData("reportType", "daily")
            .storeDurably()
            .build();
    }

    @Bean
    public Trigger reportJobTrigger() {
        return TriggerBuilder.newTrigger()
            .forJob(reportJobDetail())
            .withIdentity("reportTrigger")
            .withSchedule(
                CronScheduleBuilder.cronSchedule("0 0 2 * * ?")
                    .inTimeZone(TimeZone.getTimeZone("UTC"))
            )
            .build();
    }
}

Note

Quartz uses 7-field cron expressions (adding seconds and year), and uses ? instead of * for the day-of-week or day-of-month field when the other is specified. The formats differ between Spring, Quartz, and Unix cron — always verify which system you are targeting.

When to Choose Quartz

Feature@ScheduledQuartz
Setup complexitySimple (annotation)Moderate (config classes)
Persistent jobsNoYes (JDBC JobStore)
ClusteringNo (needs ShedLock)Built-in
Runtime managementNoYes (add/remove/pause)
Misfire handlingNoYes (configurable)

JDK Alternatives: Timer & ScheduledExecutorService

If you are not using Spring, the JDK provides two built-in scheduling mechanisms. These are best for simple standalone applications or utility scripts.

java.util.Timer (Legacy)

The Timer class has been available since Java 1.3. It is simple but has significant limitations: a single thread, no error recovery (one exception kills all tasks), and no cron expression support.

import java.util.Timer;
import java.util.TimerTask;

Timer timer = new Timer("cleanup-timer", true);  // daemon thread

timer.scheduleAtFixedRate(new TimerTask() {
    @Override
    public void run() {
        System.out.println("Running cleanup...");
        cleanupService.execute();
    }
}, 0, 60_000);  // start immediately, repeat every 60 seconds

Warning

Timer is considered legacy. If an uncaught exception occurs in a TimerTask, the entire Timer thread dies and no further tasks execute. Use ScheduledExecutorService instead.

ScheduledExecutorService (Recommended JDK Approach)

Introduced in Java 5, ScheduledExecutorService provides a thread pool, better error handling, and more control:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);

// Fixed rate: every 30 seconds
executor.scheduleAtFixedRate(() -> {
    try {
        System.out.println("Polling for updates...");
        pollingService.check();
    } catch (Exception e) {
        // Unlike Timer, one failure does NOT kill the scheduler
        log.error("Polling failed", e);
    }
}, 0, 30, TimeUnit.SECONDS);

// Fixed delay: 10 seconds after each completion
executor.scheduleWithFixedDelay(() -> {
    try {
        batchProcessor.processNext();
    } catch (Exception e) {
        log.error("Batch processing failed", e);
    }
}, 0, 10, TimeUnit.SECONDS);

// Shutdown gracefully on application exit
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    executor.shutdown();
    try {
        executor.awaitTermination(30, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        executor.shutdownNow();
    }
}));

Monitoring with CronJobPro

Java scheduled tasks run inside your application process — if the app crashes or a task fails silently, you have no visibility unless you set up external monitoring. CronJobPro uses the dead man's switch pattern: your task pings a URL on each successful run, and you get alerted when the ping stops arriving.

Adding Monitoring to a Spring @Scheduled Task

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class MonitoredReportService {

    private final RestTemplate restTemplate = new RestTemplate();
    private static final String PING_URL = "https://cronjobpro.com/ping/abc123";

    @Scheduled(cron = "0 0 2 * * *")
    public void generateDailyReport() {
        // Signal start
        try { restTemplate.getForObject(PING_URL + "/start", String.class); }
        catch (Exception ignored) {}

        try {
            reportGenerator.createDailyReport();

            // Signal success
            restTemplate.getForObject(PING_URL, String.class);
        } catch (Exception e) {
            // Signal failure
            try { restTemplate.postForObject(PING_URL + "/fail", e.getMessage(), String.class); }
            catch (Exception ignored) {}
            throw e;
        }
    }
}

For applications that trigger tasks externally, CronJobPro can call an HTTP endpoint in your Spring Boot app at the desired schedule, giving you both the trigger and the monitoring in one service. See the troubleshooting guide for common issues when scheduled tasks stop running.

Frequently Asked Questions

What is the difference between fixedRate and fixedDelay in Spring @Scheduled?

fixedRate runs the task at a constant interval measured from the start of each execution. If the task takes longer than the interval, executions may overlap (by default they queue up). fixedDelay waits for the specified duration after each execution completes before starting the next one. Use fixedRate when you want consistent frequency and fixedDelay when you want a guaranteed gap between runs.

Why does Spring use 6-field cron expressions instead of 5?

Standard Unix cron uses 5 fields: minute, hour, day-of-month, month, day-of-week. Spring adds a sixth field at the beginning for seconds, giving you second-level precision. So 0 0 2 * * * in Spring means "every day at 2:00:00 AM" while the equivalent Unix cron would be 0 2 * * *.

How do I run a scheduled task on only one instance in a Spring Boot cluster?

Spring does not provide built-in distributed locking for @Scheduled tasks. The most common solutions are: ShedLock (uses a shared database or Redis for distributed locks), Quartz Scheduler with a JDBC JobStore (provides built-in clustering support), or using an external scheduler like CronJobPro that triggers a single HTTP endpoint.

When should I use Quartz Scheduler instead of Spring @Scheduled?

Use Quartz when you need: persistent job storage (jobs survive application restarts), clustering (only one node runs each job), dynamic job scheduling at runtime (adding, modifying, or removing jobs without redeployment), misfire handling (running missed jobs after downtime), or complex trigger logic. For simple fixed-rate or cron-scheduled tasks in a single-instance application, @Scheduled is simpler and sufficient.

How do I monitor Java scheduled tasks in production?

The most reliable approach is the dead man's switch pattern: have your scheduled task ping an external monitoring service like CronJobPro at the end of each successful execution. If the ping stops arriving within the expected window, you get an alert. You can also send a /start ping before execution to track duration, and a /fail ping on exceptions.

Monitor Your Java Scheduled Tasks

Get instant alerts when your Spring or Quartz tasks fail or miss a scheduled run. Free for up to 5 monitors.

Start Monitoring Free