Interviewers love this question because it looks like a one-line annotation, but the real test is whether you understand threads, timing, and production safety.
Question: What is scheduling using @Scheduled in Spring Boot?
Answer: @Scheduled tells Spring to run a method automatically on a time rule, such as every few seconds, after a delay, or at a cron time like midnight. You enable it with @EnableScheduling, place the method on a Spring bean, and usually make it a no-arg void method. The important production idea is that it runs in background threads, so you must think about thread count, overlap, failures, and duplicate runs in multiple app instances.
Interview-Ready Answer: “I use @Scheduled when I want Spring Boot to call a method automatically on a time schedule, like every 10 seconds or every day at 1 AM. I enable it with @EnableScheduling, then choose fixedRate, fixedDelay, or cron based on whether I want regular polling, delay-after-completion, or calendar-based execution. In production, I also watch the scheduler thread pool and multi-instance duplication, because the default scheduler is single-threaded and every pod can run the same job unless I add a lock or other coordination.”
@Scheduled is Spring’s built-in way to run a method later or repeatedly without writing your own timer loop. Think of it as a small job clock inside your app: Spring remembers the rule, then calls the method when the time arrives.
@Scheduled.fixedDelay waits, fixedRate can fall behind, and a larger pool can allow overlap.| Trigger | Meaning | Best use | Gotcha |
|---|---|---|---|
fixedRate | Start every N ms | Regular polling | Can drift if work is slow |
fixedDelay | Wait N ms after finish | Slow or serial jobs | Runs less often when work is slow |
cron | Calendar schedule | Nightly or hourly jobs | Spring uses 6 fields, not 5 |
A good memory model is: fixedRate is a metronome, fixedDelay is a rest between reps, and cron is a wall calendar.
Use @Scheduled for simple in-process work: cache cleanup, health polling, report refresh, retries with safe side effects, or one-off maintenance. If you need durable jobs, retries after crash, clustering support, job history, or pause/resume, compare it with Quartz instead.
@Scheduled | Quartz |
|---|---|
| Simple setup | More setup |
| In-memory timing | Persistent jobs |
| Every instance may run it | Can cluster safely |
Startup registration is roughly O(number of scheduled methods). Runtime overhead is small, but each scheduled thread can block, so a default single-thread scheduler becomes a bottleneck fast. In real systems, a pool size of 2 to 8 is common for a few light jobs; if jobs do heavy I/O or long DB calls, keep them short and offload real work to a queue or async worker. Also remember that every pod or server instance runs the same @Scheduled method unless you add distributed locking, so it is not a cluster-safe scheduler by itself. Cron uses the JVM timezone unless you set zone; in containers, UTC is often the safest choice because daylight saving time can skip or repeat local times.
@EnableScheduling is required; without it, nothing runs.void is the cleanest return type.Real-World Story: Imagine an e-commerce checkout service that runs a scheduled job to expire unpaid carts and another job to refresh a fraud-risk cache every minute. In production, this is perfect for small maintenance work because the app already knows its own data and can act without a separate scheduler service. The bug happens when a team leaves the default single-thread scheduler and one job starts taking 20 seconds because a database index is missing. Symptoms appear as delayed logs, missed cleanup windows, growing table size, and users seeing stale risk scores. In a multi-pod deployment, the same cleanup can also run three times, causing duplicate deletions or noisy “already deleted” errors in the logs.
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Bean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
@SpringBootApplication
@EnableScheduling
public class SchedulingDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SchedulingDemoApplication.class, args);
}
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(2); // Default Spring scheduling is effectively single-threaded if you do not provide your own scheduler.
scheduler.setThreadNamePrefix("demo-scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true); // Helps the app shut down cleanly instead of interrupting jobs mid-flight.
scheduler.setAwaitTerminationSeconds(5);
return scheduler;
}
}
@Component
class DemoJobs {
private static final Logger log = LoggerFactory.getLogger(DemoJobs.class);
private final AtomicInteger fixedRateRuns = new AtomicInteger();
@Scheduled(initialDelay = 2000, fixedRate = 5000)
public void pollExternalSystem() {
int run = fixedRateRuns.incrementAndGet();
log.info("pollExternalSystem run {} started at {}", run, LocalDateTime.now());
try {
if (run == 3) {
// Simulate a failure path: a real scheduled job should log clearly and keep the system healthy.
throw new IllegalStateException("simulated downstream failure on run 3");
}
Thread.sleep(1500); // Short work: fixed-rate means the next start is measured from the previous start time.
log.info("pollExternalSystem run {} finished normally", run);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("pollExternalSystem interrupted during shutdown");
} catch (RuntimeException e) {
log.error("pollExternalSystem failed but the scheduler stays alive: {}", e.getMessage());
}
}
@Scheduled(cron = "0 */1 * * * *", zone = "UTC")
public void minuteHeartbeat() {
log.info("minuteHeartbeat fired at {} UTC", LocalDateTime.now());
}
@Scheduled(initialDelay = 1000, fixedDelay = 10000)
public void expensiveReportRefresh() {
log.info("expensiveReportRefresh started at {}", LocalDateTime.now());
try {
Thread.sleep(7000);
// fixedDelay waits after the method completes, so this job cannot start the next run until the work is done.
log.info("expensiveReportRefresh completed at {}", LocalDateTime.now());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("expensiveReportRefresh interrupted");
}
}
}
Follow-up & Tricky Questions:
TaskScheduler bean, usually ThreadPoolTaskScheduler, and set a pool size such as 2, 4, or 8 depending on how many jobs may run at once.fixedRate and fixedDelay? fixedRate measures from the start of one run to the start of the next, while fixedDelay waits after the previous run finishes, so it naturally avoids overlap.@Scheduled work in multiple instances? Yes, every instance will run it unless you add coordination. In Kubernetes or a multi-node deployment, that usually means duplicate executions unless you use a distributed lock or leader election.zone attribute, such as zone = "UTC", so the job does not depend on the server’s local clock.fixedDelayString, fixedRateString, or cron placeholders so operations can change timing without code changes.Tricky gotchas:
@Scheduled durable after restart? No. If the JVM goes down, any missed executions are gone because the schedule is not persisted.@Scheduled guarantee exact millisecond timing? No. It is best effort, not a real-time timer, so GC pauses, load, and thread contention can shift execution.Common Mistakes:
@EnableScheduling — correction: add it on a configuration or main application class.ThreadPoolTaskScheduler with a small but real pool size.@Scheduled for non-idempotent work in many replicas — correction: add a lock, leader election, or a dedicated scheduler.UTC or explicitly set zone.Memory Hook: fixedRate = metronome, fixedDelay = rest between reps, cron = calendar appointment. If you can say those three phrases under pressure, you can explain the whole topic.
Cheat Sheet:
@EnableScheduling.fixedRate for regular polling, fixedDelay for serial work, and cron for calendar times.Practice Tasks:
@Scheduled(fixedRate = 3000) method that logs every 3 seconds.fixedDelay and watch how the timing changes when you add Thread.sleep(4000).