Fixed-rate and fixed-delay are two ways to repeat a background task at a regular interval with @Scheduled. Fixed rate counts the interval from the start of the previous run, so the rhythm stays steady. Fixed delay counts the interval from the end of the previous run, so there is always a pause between runs. Choosing the right one decides whether a slow task keeps up with the clock or waits for itself to finish.
Features
fixedRatekeeps a steady start-to-start rhythmfixedDelayguarantees a gap after each run finishes- Same task is never run twice at the same time (a slow fixed-rate task runs back-to-back instead)
initialDelaywaits before the first run- Intervals read from
application.propertieswithfixedRateStringandfixedDelayString - Thread pool size controls whether different tasks can run side by side
- Interval unit is milliseconds (or
timeUnitcan be set in Spring 6) - Starts automatically with
@EnableScheduling, no trigger needed
Hands-on Experiment: Creating a spring boot application to understand two common approaches for repeatedly executing background tasks.
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: periodic
- Name: periodic
- Package name: com.example.periodic
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver.
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>student-fixed-rate-delay-demo</artifactId>
<version>1.0.0</version>
<name>student-fixed-rate-delay-demo</name>
<description>Fixed-rate and Fixed-delay Execution - periodic status and Student report tasks</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.properties
spring.application.name=student-fixed-rate-delay-demo
# ---------- MySQL ----------
# Create the database first: CREATE DATABASE student_periodic_db;
spring.datasource.url=jdbc:mysql://localhost:3306/student_periodic_db
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
server.port=8082
# ---------- Scheduler thread pool ----------
# 4 tasks run in this app. With 4 threads they run side by side (default pool size is 1).
spring.task.scheduling.pool.size=4
spring.task.scheduling.thread-name-prefix=periodic-
# ---------- Logging ----------
logging.level.root=INFO
logging.level.com.example.periodic=DEBUG
logging.pattern.console=%d{HH:mm:ss.SSS} | %-5level | %thread | %msg%n
# ---------- Task timings (milliseconds) ----------
# Wait before the first run of every task
app.task.initial-delay-ms=5000
# TASK 1 - FIXED RATE: application status check every 10 seconds (start to start)
app.task.status-rate-ms=10000
# TASK 2 - FIXED DELAY: Student report, next run 15 seconds AFTER the previous one finished
app.task.report-delay-ms=15000
# Simulated time the report needs to finish
app.task.report-processing-ms=3000
# TASKS 3 and 4 - side-by-side comparison of fixed rate and fixed delay
# Both: period/delay = 5 seconds, work = 2 seconds
# fixed rate -> starts every 5 s (gap between starts = 5 s)
# fixed delay -> starts every 5 + 2 = 7 s (gap between starts = 7 s)
# Try app.task.demo-work-ms=8000 : the work is LONGER than the period.
# fixed rate -> runs back-to-back (gap = 8 s), runs never overlap
# fixed delay -> gap = 13 s
app.task.demo-period-ms=5000
app.task.demo-work-ms=2000Code language: PHP (php)
Step 5: Create Entity Class
package com.example.periodic.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
// One row per Student report (written by the FIXED DELAY task)
@Entity
@Table(name = "report_logs")
public class ReportLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
private long durationMs;
private long totalStudents;
private Double averageMarks;
@Column(length = 1000)
private String courseSummary;
public ReportLog() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public LocalDateTime getStartedAt() { return startedAt; }
public void setStartedAt(LocalDateTime startedAt) { this.startedAt = startedAt; }
public LocalDateTime getFinishedAt() { return finishedAt; }
public void setFinishedAt(LocalDateTime finishedAt) { this.finishedAt = finishedAt; }
public long getDurationMs() { return durationMs; }
public void setDurationMs(long durationMs) { this.durationMs = durationMs; }
public long getTotalStudents() { return totalStudents; }
public void setTotalStudents(long totalStudents) { this.totalStudents = totalStudents; }
public Double getAverageMarks() { return averageMarks; }
public void setAverageMarks(Double averageMarks) { this.averageMarks = averageMarks; }
public String getCourseSummary() { return courseSummary; }
public void setCourseSummary(String courseSummary) { this.courseSummary = courseSummary; }
}
package com.example.periodic.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
// One row per application-status check (written by the FIXED RATE task)
@Entity
@Table(name = "status_logs")
public class StatusLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private LocalDateTime checkedAt;
private String status;
private long totalStudents;
private long usedMemoryMb;
private long uptimeSeconds;
public StatusLog() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public LocalDateTime getCheckedAt() { return checkedAt; }
public void setCheckedAt(LocalDateTime checkedAt) { this.checkedAt = checkedAt; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public long getTotalStudents() { return totalStudents; }
public void setTotalStudents(long totalStudents) { this.totalStudents = totalStudents; }
public long getUsedMemoryMb() { return usedMemoryMb; }
public void setUsedMemoryMb(long usedMemoryMb) { this.usedMemoryMb = usedMemoryMb; }
public long getUptimeSeconds() { return uptimeSeconds; }
public void setUptimeSeconds(long uptimeSeconds) { this.uptimeSeconds = uptimeSeconds; }
}
package com.example.periodic.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
private String course;
private Integer marks;
public Student() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getCourse() { return course; }
public void setCourse(String course) { this.course = course; }
public Integer getMarks() { return marks; }
public void setMarks(Integer marks) { this.marks = marks; }
}
Step 6: Create below Repository Interface
package com.example.periodic.repository;
import com.example.periodic.entity.ReportLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ReportLogRepository extends JpaRepository<ReportLog, Long> {
List<ReportLog> findTop10ByOrderByIdDesc();
}
package com.example.periodic.repository;
import com.example.periodic.entity.StatusLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StatusLogRepository extends JpaRepository<StatusLog, Long> {
List<StatusLog> findTop10ByOrderByIdDesc();
}
package com.example.periodic.repository;
import com.example.periodic.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
@Query("select avg(s.marks) from Student s")
Double findAverageMarks();
// Each row: [course, number of students]
@Query("select s.course, count(s) from Student s group by s.course order by s.course")
List<Object[]> countStudentsByCourse();
}
Step 7: Create below Service class
package com.example.periodic.service;
import com.example.periodic.entity.Student;
import com.example.periodic.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student add(Student student) {
if (studentRepository.existsByEmail(student.getEmail())) {
throw new IllegalArgumentException("Email already registered: " + student.getEmail());
}
return studentRepository.save(student);
}
// Adds many students at once; duplicate emails are skipped
public List<Student> addAll(List<Student> students) {
List<Student> saved = new ArrayList<>();
for (Student s : students) {
if (!studentRepository.existsByEmail(s.getEmail())) {
saved.add(studentRepository.save(s));
}
}
return saved;
}
public List<Student> getAll() {
return studentRepository.findAll();
}
}
package com.example.periodic.service;
import com.example.periodic.entity.ReportLog;
import com.example.periodic.entity.StatusLog;
import com.example.periodic.repository.ReportLogRepository;
import com.example.periodic.repository.StatusLogRepository;
import com.example.periodic.repository.StudentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class TaskService {
private static final Logger log = LoggerFactory.getLogger(TaskService.class);
private static final String STATUS = "status-check (fixed rate)";
private static final String REPORT = "student-report (fixed delay)";
private static final String DEMO_RATE = "demo-A (fixed rate)";
private static final String DEMO_DELAY = "demo-B (fixed delay)";
private final StudentRepository studentRepository;
private final StatusLogRepository statusLogRepository;
private final ReportLogRepository reportLogRepository;
@Value("${app.task.report-processing-ms}")
private long reportProcessingMs;
@Value("${app.task.report-delay-ms}")
private long reportDelayMs;
@Value("${app.task.demo-work-ms}")
private long demoWorkMs;
// ----- bookkeeping, so we can SEE the difference between the two styles -----
private final Map<String, Long> lastStart = new ConcurrentHashMap<>();
private final Map<String, Long> lastGapMs = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> runCount = new ConcurrentHashMap<>();
private final AtomicBoolean paused = new AtomicBoolean(false);
public TaskService(StudentRepository studentRepository,
StatusLogRepository statusLogRepository,
ReportLogRepository reportLogRepository) {
this.studentRepository = studentRepository;
this.statusLogRepository = statusLogRepository;
this.reportLogRepository = reportLogRepository;
}
// ==================================================================
// TASK 1 - FIXED RATE: application status check
// Runs every 10 s, counted from the START of the previous run.
// Good for light, regular checks that must keep a steady rhythm.
// ==================================================================
@Scheduled(fixedRateString = "${app.task.status-rate-ms}",
initialDelayString = "${app.task.initial-delay-ms}")
public void applicationStatusCheck() {
if (paused.get()) {
log.debug("[FIXED RATE] Status check skipped (tasks are paused)");
return;
}
long gap = markStart(STATUS);
log.info("[FIXED RATE] Application status check #{} started (gap since previous start: {})",
runCount.get(STATUS).get(), gapText(gap));
try {
Runtime rt = Runtime.getRuntime();
StatusLog entry = new StatusLog();
entry.setCheckedAt(LocalDateTime.now());
entry.setStatus("UP");
entry.setTotalStudents(studentRepository.count());
entry.setUsedMemoryMb((rt.totalMemory() - rt.freeMemory()) / (1024 * 1024));
entry.setUptimeSeconds(ManagementFactory.getRuntimeMXBean().getUptime() / 1000);
statusLogRepository.save(entry);
log.info("[FIXED RATE] Status UP | students={} | memory={} MB | uptime={} s",
entry.getTotalStudents(), entry.getUsedMemoryMb(), entry.getUptimeSeconds());
} catch (Exception ex) {
// Always catch inside a job. If the status check cannot reach the database, the app is not healthy.
log.error("[FIXED RATE] Status check failed", ex);
}
}
// ==================================================================
// TASK 2 - FIXED DELAY: Student report
// The next run starts 15 s AFTER the previous run FINISHED.
// Good for heavier jobs that must never start while the last one is still working.
// ==================================================================
@Scheduled(fixedDelayString = "${app.task.report-delay-ms}",
initialDelayString = "${app.task.initial-delay-ms}")
public void studentReport() {
if (paused.get()) {
log.debug("[FIXED DELAY] Student report skipped (tasks are paused)");
return;
}
long gap = markStart(REPORT);
LocalDateTime started = LocalDateTime.now();
log.info("[FIXED DELAY] Student report #{} started (gap since previous start: {})",
runCount.get(REPORT).get(), gapText(gap));
try {
sleep(reportProcessingMs); // pretend the report takes time
long total = studentRepository.count();
Double avg = studentRepository.findAverageMarks();
List<String> parts = new ArrayList<>();
for (Object[] row : studentRepository.countStudentsByCourse()) {
parts.add((row[0] == null ? "Not set" : row[0]) + "=" + row[1]);
}
ReportLog report = new ReportLog();
report.setStartedAt(started);
report.setFinishedAt(LocalDateTime.now());
report.setDurationMs(System.currentTimeMillis() - lastStart.get(REPORT));
report.setTotalStudents(total);
report.setAverageMarks(avg == null ? null : Math.round(avg * 100.0) / 100.0);
report.setCourseSummary(String.join(", ", parts));
reportLogRepository.save(report);
log.info("[FIXED DELAY] Student report #{} FINISHED in {} ms | students={} | avg marks={} | next run starts in {} ms",
runCount.get(REPORT).get(), report.getDurationMs(), total, report.getAverageMarks(),
reportDelayMs);
} catch (Exception ex) {
log.error("[FIXED DELAY] Student report failed", ex);
}
}
// ==================================================================
// TASKS 3 and 4 - the same period (5 s) and the same work (2 s), only the style differs.
// Compare the "gap since previous start" in the console:
// fixed rate -> 5000 ms
// fixed delay -> 7000 ms (2000 ms work + 5000 ms delay)
// ==================================================================
@Scheduled(fixedRateString = "${app.task.demo-period-ms}",
initialDelayString = "${app.task.initial-delay-ms}")
public void demoFixedRate() {
if (paused.get()) {
return;
}
long gap = markStart(DEMO_RATE);
log.info("[DEMO A - FIXED RATE ] run #{} start | gap since previous start: {}",
runCount.get(DEMO_RATE).get(), gapText(gap));
sleep(demoWorkMs);
log.info("[DEMO A - FIXED RATE ] run #{} end | work took {} ms", runCount.get(DEMO_RATE).get(), demoWorkMs);
}
@Scheduled(fixedDelayString = "${app.task.demo-period-ms}",
initialDelayString = "${app.task.initial-delay-ms}")
public void demoFixedDelay() {
if (paused.get()) {
return;
}
long gap = markStart(DEMO_DELAY);
log.info("[DEMO B - FIXED DELAY] run #{} start | gap since previous start: {}",
runCount.get(DEMO_DELAY).get(), gapText(gap));
sleep(demoWorkMs);
log.info("[DEMO B - FIXED DELAY] run #{} end | work took {} ms", runCount.get(DEMO_DELAY).get(), demoWorkMs);
}
// ==================================================================
// Helpers used by the controller
// ==================================================================
public void pause() {
paused.set(true);
log.warn("All periodic tasks paused");
}
public void resume() {
paused.set(false);
log.warn("All periodic tasks resumed");
}
public boolean isPaused() {
return paused.get();
}
public Map<String, Object> stats() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("paused", paused.get());
for (String task : List.of(STATUS, REPORT, DEMO_RATE, DEMO_DELAY)) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("runs", runCount.containsKey(task) ? runCount.get(task).get() : 0);
info.put("lastGapBetweenStartsMs", lastGapMs.getOrDefault(task, -1L));
result.put(task, info);
}
return result;
}
public List<StatusLog> latestStatusLogs() {
return statusLogRepository.findTop10ByOrderByIdDesc();
}
public List<ReportLog> latestReportLogs() {
return reportLogRepository.findTop10ByOrderByIdDesc();
}
// Remembers when a task started and returns the time since its previous start (-1 for the first run)
private long markStart(String task) {
long now = System.currentTimeMillis();
Long previous = lastStart.put(task, now);
runCount.computeIfAbsent(task, k -> new AtomicLong()).incrementAndGet();
long gap = previous == null ? -1 : now - previous;
lastGapMs.put(task, gap);
return gap;
}
private String gapText(long gap) {
return gap < 0 ? "first run" : gap + " ms";
}
private void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
Step 8: Create Controller class
package com.example.periodic.controller;
import com.example.periodic.entity.Student;
import com.example.periodic.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/students")
public class StudentController {
private final StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@PostMapping
public ResponseEntity<?> add(@RequestBody Student student) {
try {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.add(student));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
@PostMapping("/bulk")
public ResponseEntity<List<Student>> addAll(@RequestBody List<Student> students) {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.addAll(students));
}
@GetMapping
public List<Student> getAll() {
return studentService.getAll();
}
}
package com.example.periodic.controller;
import com.example.periodic.entity.ReportLog;
import com.example.periodic.entity.StatusLog;
import com.example.periodic.service.TaskService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
// Number of runs and the gap between the last two starts of each task
@GetMapping("/stats")
public Map<String, Object> stats() {
return taskService.stats();
}
// Last 10 application-status checks (saved by the FIXED RATE task)
@GetMapping("/status-logs")
public List<StatusLog> statusLogs() {
return taskService.latestStatusLogs();
}
// Last 10 Student reports (saved by the FIXED DELAY task)
@GetMapping("/report-logs")
public List<ReportLog> reportLogs() {
return taskService.latestReportLogs();
}
@PostMapping("/pause")
public Map<String, Object> pause() {
taskService.pause();
return Map.of("paused", taskService.isPaused());
}
@PostMapping("/resume")
public Map<String, Object> resume() {
taskService.resume();
return Map.of("paused", taskService.isPaused());
}
}
Step 9: Main Application
package com.example.periodic;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class PeriodicApplication {
public static void main(String[] args) {
SpringApplication.run(PeriodicApplication.class, args);
}
}
Step 10: Run the StudentRestCurdApplication

