Scheduling with @Scheduled
Some work doesn't happen in response to a request — it happens every night at 2am, or every 30 seconds, forever. This module is how Spring Boot runs code on a clock instead of on a request.
Learning objectives
- Beginner: Enable scheduling and write a method that runs on a fixed interval.
- Intermediate: Use cron expressions to schedule a task for a specific time of day.
- Advanced: Explain why the default scheduler runs single-threaded, and configure a thread pool when tasks can overlap or run long.
◆ The problem
Some work is inherently time-based, not request-based: expiring stale sessions, sending a daily digest email, polling a third-party API for updates. None of that has an incoming HTTP request to attach to.
@EnableScheduling on a configuration class turns on Spring's scheduling infrastructure; @Scheduled on a method tells it to run that method automatically.
@Configuration @EnableScheduling public class SchedulingConfig {} @Component public class SessionCleanupTask { @Scheduled(fixedRate = 60_000) // every 60 seconds public void expireStaleSessions() { ... } }
fixedRate/fixedDelay cover "every N seconds," but "every day at 2am" needs a cron expression instead.
@Scheduled(cron = "0 0 2 * * *") // second minute hour day month weekday public void sendDailyDigest() { ... }
| Field | Example | Meaning |
|---|---|---|
| second | 0 | at second 0 |
| minute | 0 | at minute 0 |
| hour | 2 | at 2 AM |
| day-of-month | * | every day |
| month | * | every month |
| day-of-week | * | every weekday |
⚠ Common real-world trap
By default, Spring runs ALL @Scheduled methods on a single shared thread — one slow-running scheduled task silently delays every other scheduled task in the application, since none of them can run until the thread frees up. A team can go months without noticing this until two unrelated scheduled jobs start mysteriously running late together.
The fix is configuring a proper thread pool:
@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); return scheduler; }
💻 Code example
@Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); return scheduler; }
@Scheduled runs independently on EVERY instance of your application — if you deploy 3 replicas in Kubernetes, that "every night at 2am" job runs 3 times, not once, unless you explicitly coordinate it. For anything that must run exactly once across a cluster (not once per instance), you need a distributed lock (via Redis or a database row) or a dedicated scheduling system like Quartz configured for clustering, not plain @Scheduled.
✓ Quick recap
@EnableScheduling+@Scheduledruns code on a timer instead of a request.- Use
fixedRate/fixedDelayfor simple intervals, cron expressions for specific times. - The default scheduler is single-threaded — configure a
TaskSchedulerbean with a pool for anything that can run long or overlap. @Scheduledruns per-instance, not per-cluster — coordinate with a distributed lock if a job must run exactly once across replicas.
Want a visual for this concept?
Generate a diagram tailored to “Scheduling with @Scheduled” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →