Spring Framework
Scheduled Tasks
Multi-Instance Deployment
Java Programming
Task Scheduling

Spring and scheduled tasks on multiple instances

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In Spring, @Scheduled runs inside each application instance. That means if you deploy three identical instances, the scheduled method will normally run three times unless you add coordination. For local development this is often fine, but in production it can create duplicate emails, repeated billing jobs, or conflicting cleanup tasks.

Why the Problem Happens

A scheduled method is not cluster-aware by default. Spring sees only the current JVM, not the rest of your deployment.

For example:

java
1import org.springframework.scheduling.annotation.Scheduled;
2import org.springframework.stereotype.Component;
3
4@Component
5public class ReportJob {
6
7    @Scheduled(cron = "0 */5 * * * *")
8    public void run() {
9        System.out.println("Generating report...");
10    }
11}

If this service runs on four pods, all four pods will run that method every five minutes.

Decide Whether the Job Should Run Once or Many Times

Before fixing anything, classify the task:

  • some jobs are safe on every instance, such as local cache refresh
  • some jobs must run once globally, such as settlement or invoice generation
  • some jobs can run many times if they are idempotent and partition-aware

If the job truly must run once, you need distributed coordination rather than only local scheduling.

A Practical Solution: Distributed Locking

One common approach in Spring applications is to acquire a shared lock before running the task. If one instance gets the lock, the others skip execution.

java
1import org.springframework.scheduling.annotation.Scheduled;
2import org.springframework.stereotype.Component;
3
4@Component
5public class SyncJob {
6
7    private final JobLockService jobLockService;
8    private final SyncService syncService;
9
10    public SyncJob(JobLockService jobLockService, SyncService syncService) {
11        this.jobLockService = jobLockService;
12        this.syncService = syncService;
13    }
14
15    @Scheduled(cron = "0 */5 * * * *")
16    public void run() {
17        if (!jobLockService.tryLock("syncJob")) {
18            return;
19        }
20
21        try {
22            syncService.runSync();
23        } finally {
24            jobLockService.unlock("syncJob");
25        }
26    }
27}

In practice, the lock backend is usually a database row, Redis key, or a library such as ShedLock. The key point is that the lock lives outside the current JVM.

Idempotency Still Matters

A distributed lock reduces duplicates, but it does not eliminate all failure modes. A process can crash halfway through a job, or a lock can expire while work is still in progress. That is why scheduled tasks should still be designed to be idempotent.

Good patterns include:

  • writing durable job state
  • checking whether a period has already been processed
  • using unique business keys to prevent duplicate side effects

Locking is one protection layer. Idempotent job logic is another.

Scheduling Thread Pool and Overlap

Even on one instance, problems appear if a task runs longer than its schedule interval. Spring's scheduler pool controls local concurrency but does not solve cluster duplication.

yaml
1spring:
2  task:
3    scheduling:
4      pool:
5        size: 4

You should log job start time, finish time, and lock outcome so overlap problems are visible during operations.

When to Use Something Other Than @Scheduled

If the task is business-critical, @Scheduled inside the application may not be the strongest design.

Alternatives include:

  • Kubernetes CronJob
  • Quartz with a clustered job store
  • external workflow schedulers

Those tools usually provide stronger visibility, retry control, and execution history than simple in-process scheduling.

Common Pitfalls

  • Assuming @Scheduled runs only once across a cluster.
  • Adding a distributed lock but not making the job idempotent.
  • Ignoring long-running jobs that can overlap their next schedule.
  • Using local scheduler pool settings as if they solved multi-instance duplication.
  • Keeping critical workflows inside app-local scheduling when stronger orchestration is needed.

Summary

  • Spring scheduled methods run independently on every deployed instance.
  • Jobs that must run once globally need shared coordination.
  • Distributed locks help, but idempotency is still required.
  • Monitor duration and overlap risk, not only success or failure.
  • For critical workflows, consider dedicated job infrastructure instead of only @Scheduled.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.