Skip to content

Distributed Scheduler (Part 2) | Cron Job, FixedRate, FixedDelay

The Factory Shift Analogy

A large manufacturing plant operates around the clock. Some machines must run every 30 minutes regardless of when the previous cycle ended (fixed rate). Others must cool down for exactly 10 minutes after each cycle before the next one can begin (fixed delay). A few machines must run at specific clock times — 06:00, 12:00, 18:00 — to synchronise with shift changes, no matter when the previous run finished (cron). Each type of schedule serves a different operational need, and picking the wrong one wastes energy or breaks the production line.

Spring Boot gives you all three modes through a single annotation. Understanding when to use each — and how each handles slow tasks — prevents subtle production bugs.


Prerequisites

As established in the previous chapter, Spring Boot scheduling is a wrapper over Java's ScheduledThreadPoolExecutor. Every mode in this chapter maps to one of the three executor methods:

Spring Boot @Scheduled attributeUnderlying executor method
fixedRatescheduleAtFixedRate
fixedDelayscheduleWithFixedDelay
initialDelay onlyschedule (one time)
cronComputed interval, re‑queued same as fixed rate

Step 1 — Enable Scheduling

Before any @Scheduled annotation is processed, you must place @EnableScheduling on a configuration class. Without it, Spring Boot does not load the scheduling infrastructure and the @Scheduled annotations are silently ignored.

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

@SpringBootApplication
@EnableScheduling   // Required — activates the scheduling subsystem
public class SchedulerApplication {
    public static void main(String[] args) {
        SpringApplication.run(SchedulerApplication.class, args);
    }
}

Fixed Rate Scheduling

Fixed rate runs the task repeatedly at a fixed rate, using previous start time + period to calculate the next execution time.

java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalTime;

@Component
public class FixedRateScheduler {

    /**
     * initialDelay: wait 3 seconds after startup before the first run.
     * fixedRate:    after each start, schedule the next run 5 seconds later.
     * Both values are in milliseconds.
     */
    @Scheduled(initialDelay = 3_000, fixedRate = 5_000)
    public void runAtFixedRate() {
        System.out.println("Fixed-rate task started at: " + LocalTime.now());
        // ... business logic ...
    }
}

Sample output:

Application started at 10:38:00
Fixed-rate task started at: 10:38:03   (initial delay = 3 s)
Fixed-rate task started at: 10:38:08   (3 + 5)
Fixed-rate task started at: 10:38:13   (8 + 5)
Fixed-rate task started at: 10:38:18   (13 + 5)

If initialDelay is omitted, the default is 0 — the task runs immediately after startup.

When the task is slow: if the task started at second 3 and finishes at second 15, the scheduler computes 3 + 5 = 8 (past), then 8 + 5 = 13 (past), then 13 + 5 = 18 (future). The next run is at second 18. The task never runs in parallel with itself.


Fixed Delay Scheduling

Fixed delay runs the task repeatedly, waiting a fixed amount of time after each completion before starting the next run.

java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalTime;

@Component
public class FixedDelayScheduler {

    /**
     * initialDelay: 1 second before the first run.
     * fixedDelay:   wait 5 seconds AFTER each task finishes before the next.
     */
    @Scheduled(initialDelay = 1_000, fixedDelay = 5_000)
    public void runWithFixedDelay() throws InterruptedException {
        LocalTime start = LocalTime.now();
        System.out.println("Fixed-delay task started at: " + start);

        Thread.sleep(3_000); // Simulate 3-second work

        System.out.println("Fixed-delay task finished at: " + LocalTime.now());
    }
}

Sample output:

Application started at 10:38:31
Fixed-delay task started at:  10:38:32   (initial delay = 1 s)
Fixed-delay task finished at: 10:38:35   (3 s of work)
Fixed-delay task started at:  10:38:40   (35 + 5 = 40)
Fixed-delay task finished at: 10:38:43
Fixed-delay task started at:  10:38:48   (43 + 5 = 48)

The delay is measured from finish time — so a slow task automatically pushes the next run further into the future. This prevents overlapping runs and ensures adequate rest time between executions.


One‑Time Scheduling

For tasks that should run exactly once after startup:

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

@Component
public class OneTimeScheduler {

    /**
     * initialDelay is MANDATORY for one-time scheduling.
     * If omitted, an exception is thrown at startup.
     * No fixedRate or fixedDelay means the task runs only once.
     */
    @Scheduled(initialDelay = 2_000)
    public void runOnce() {
        System.out.println("One-time task executed at startup + 2 seconds.");
        // e.g. warm up a cache, run a data migration
    }
}

Important: initialDelay is mandatory here. For fixedRate and fixedDelay tasks, omitting initialDelay defaults to 0. For a one‑time task, omitting it raises an exception.


Cron Job Scheduling

Cron jobs are repeated tasks that fire at wall clock times rather than measuring intervals from the previous run. They follow a six‑field expression and align to the actual clock.

The Six‑Field Cron Expression

┌─────────── 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, where 0 and 7 are both Sunday, or SUN–SAT)
│ │ │ │ │ │
* * * * * *

Special characters:

CharacterMeaningExample
*Every value in this field* * * * * * = every second
,List of specific values0,20 in seconds = at second 0 and second 20
-Range (in expression fields only — this is part of the syntax, not prose)9-15 in hours = hours 9 through 15
/Step interval from a start4/10 in seconds = 4, 14, 24, 34, 44, 54
LLast (day of month or day of week only)L in day of month = last day of the month
?Ignore this field (removes ambiguity between day of month and day of week)

Wall Clock Semantics — Key Difference from Fixed Rate

With a fixed rate, the next run is computed relative to the previous start time. With a cron, the next run is chosen from the wall clock schedule.

Example: cron set to run every 10 minutes (*/10 in the minute field), task starts at 10:10, takes 25 minutes.

Clock timeEvent
10:10Task starts
10:20Cron slot — skipped, task still running
10:30Cron slot — skipped, task still running
10:35Task finishes
10:40Next cron slot in the future — task re‑queued for 10:40

At 10:35 the scheduler computes: next wall clock slot is 10:40. The task is re‑queued with execution time 10:40.

Cron Usage in Spring Boot

java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Component
public class CronScheduler {

    /**
     * Cron: 0 0 9 * * MON-FRI
     * Reads as: at second 0, minute 0, hour 9, every day of month,
     *           every month, only Monday through Friday.
     * Zone is explicitly set to avoid using the JVM default.
     */
    @Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Kolkata")
    public void weekdayMorningJob() {
        System.out.println("Weekday morning job at: " + LocalDateTime.now());
    }

    /** Every 5 seconds: 0/5 * * * * * */
    @Scheduled(cron = "0/5 * * * * *")
    public void everyFiveSeconds() {
        System.out.println("Running every 5 seconds");
    }

    /** Every day at 11:15 AM: 0 15 11 * * ? */
    @Scheduled(cron = "0 15 11 * * ?")
    public void dailyElevenFifteen() {
        System.out.println("Daily 11:15 AM job");
    }

    /** Every Sunday at midnight: 0 0 0 ? * SUN */
    @Scheduled(cron = "0 0 0 ? * SUN")
    public void sundayMidnight() {
        System.out.println("Sunday midnight job");
    }

    /** First day of every month at midnight: 0 0 0 1 * ? */
    @Scheduled(cron = "0 0 0 1 * ?")
    public void firstDayOfMonth() {
        System.out.println("First day of month job");
    }

    /** Last day of every month at midnight: 0 0 0 L * ? */
    @Scheduled(cron = "0 0 0 L * ?")
    public void lastDayOfMonth() {
        System.out.println("Last day of month job");
    }
}

Practical Cron Expression Reference

RequirementCron Expression
Every 5 seconds0/5 * * * * *
Every 4 minutes0 0/4 * * * *
Every day at 11:15 AM0 15 11 * * ?
Every weekday at 9 AM0 0 9 ? * MON-FRI
Every Sunday at midnight0 0 0 ? * SUN
First of every month at midnight0 0 0 1 * ?
Last day of every month at midnight0 0 0 L * ?

Fixed Rate vs Fixed Delay vs Cron — Side by Side

Timeline with period = 3 minutes, task takes 2 minutes:

Fixed Rate  (next = start + period):
  START─────FINISH     START─────FINISH
  0:00      0:02    0:03      0:05    0:06 ...

Fixed Delay (next = finish + delay):
  START─────FINISH        START─────FINISH
  0:00      0:02  0:05   0:05       0:07  0:10 ...

Cron        (next = next wall-clock slot):
  START─────FINISH   [0:03 slot missed]  START────
  0:00      0:02                         0:06    ...

Configuring the Thread Pool Size

The default Spring Boot scheduling pool has a single thread. All @Scheduled methods share that one thread, so a slow task can block all others.

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

Or with a bean:

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

@Configuration
public class SchedulingConfig {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler ts = new ThreadPoolTaskScheduler();
        ts.setPoolSize(5);
        ts.setThreadNamePrefix("app-scheduler-");
        ts.setWaitForTasksToCompleteOnShutdown(true);
        ts.setAwaitTerminationSeconds(60);
        return ts;
    }
}

Using Property Placeholders for Schedule Values

Hard‑coding millisecond values in annotations makes it difficult to adjust timing without redeployment:

java
@Component
public class ConfigurableScheduler {

    /**
     * Values sourced from application.properties.
     * scheduler.fixed-rate-ms=60000
     * scheduler.initial-delay-ms=5000
     */
    @Scheduled(
        fixedRateString  = "${scheduler.fixed-rate-ms:60000}",
        initialDelayString = "${scheduler.initial-delay-ms:5000}"
    )
    public void configurableTask() {
        System.out.println("Running configurable task");
    }

    @Scheduled(cron = "${scheduler.cron:0 0 * * * ?}")
    public void configurableCronTask() {
        System.out.println("Running cron-based configurable task");
    }
}

The : provides a default value in case the property is not defined, making the scheduler safe to run locally without a full configuration file.


Complete Example: Subscription Expiry Scheduler

java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

@Component
public class SubscriptionExpiryScheduler {

    private static final Logger log = LoggerFactory.getLogger(SubscriptionExpiryScheduler.class);

    private final SubscriptionService subscriptionService;

    public SubscriptionExpiryScheduler(SubscriptionService subscriptionService) {
        this.subscriptionService = subscriptionService;
    }

    /**
     * Runs every day at 1:00 AM IST.
     * Uses wall clock semantics — fires precisely at 1 AM regardless of run duration.
     */
    @Scheduled(cron = "0 0 1 * * ?", zone = "Asia/Kolkata")
    public void processExpiredSubscriptions() {
        log.info("Starting subscription expiry job at {}", LocalDateTime.now());
        try {
            subscriptionService.expireAll();
        } catch (Exception e) {
            log.error("Subscription expiry job failed", e);
        }
        log.info("Subscription expiry job completed at {}", LocalDateTime.now());
    }
}

Interview Questions & Pitfalls

Q1: What happens if @EnableScheduling is not added to the application?

A: Spring Boot does not load the scheduling infrastructure. All @Scheduled annotations are silently ignored — no error is thrown, no task ever runs. This is a common pitfall in new projects where scheduling appears to work during development but the tasks are never actually executed.


Q2: What is the difference between fixedRate and fixedDelay?

A: fixedRate measures the interval from start to start (next = previous start + period). fixedDelay measures the interval from finish to start (next = previous finish + delay). When a task is slower than its period, fixedRate catches up to the original schedule while fixedDelay always provides a full rest gap after completion. For tasks that must not run concurrently and require breathing room between runs, fixedDelay is safer.


Q3: How does a cron job handle a missed slot?

A: A cron job does not back‑fill missed slots. If the task was still running when a slot was due, that slot is simply skipped. The scheduler computes the next future wall clock slot and schedules the task for that time. This mirrors how Unix cron behaves.


Q4: What is the ? character in a cron expression and when is it necessary?

A: ? means "ignore this field" and is only valid in the day of month and day of week positions. It is used to remove ambiguity: if you want "every Monday", you cannot also say "every day of the month" in the same expression without conflicting semantics. Using ? in one field tells the scheduler to derive the schedule only from the other field.


Q5: Is initialDelay mandatory for one‑time scheduling?

A: Yes. For a task with no fixedRate, fixedDelay, or cron — meaning it should run once — initialDelay is mandatory. Omitting it causes an exception at startup. For repeating tasks, omitting initialDelay defaults to 0 (immediate start).


Q6: The cron expression 0 0/4 * * * * — what does / mean here?

A: The / character defines a step: start value / step size. 0/4 in the minute field means "start at minute 0, then every 4 minutes": 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56. The * in all other fields means the job fires every hour on those minutes.


Q7: You have two @Scheduled methods on the default thread pool. One task sleeps for 10 minutes. What happens to the other task?

A: With the default pool size of 1, all @Scheduled methods share a single thread. The 10‑minute sleeping task holds the thread, blocking all other scheduled tasks from running. The fix is to increase spring.task.scheduling.pool.size to at least 2, or configure separate TaskScheduler beans.