Spring cron vs Unix cron vs Quartz
Most broken schedules come from copying an expression written for a different cron dialect. The three look alike but disagree on field count, day numbering and how the two day fields combine.
| Spring (@Scheduled) | Unix crontab | Quartz | |
|---|---|---|---|
| Fields | 6 — seconds first | 5 — no seconds | 6 or 7 (optional year) |
| Sunday | 0 or 7 (or SUN) | 0 or 7 | 1 (or SUN) |
| Monday | 1 | 1 | 2 |
| Day-of-month + day-of-week | both must match | either matches | one must be ? |
L, W, # | yes | no | yes |
| Macros | @hourly, @daily, @weekly, @monthly, @yearly | same, plus @reboot | no |
Field reference
| Position | Field | Values | Special |
|---|---|---|---|
| 1 | Second | 0–59 | * , - / |
| 2 | Minute | 0–59 | * , - / |
| 3 | Hour | 0–23 | * , - / |
| 4 | Day of month | 1–31 | * ? , - / L L-n nW LW |
| 5 | Month | 1–12 or JAN–DEC | * , - / |
| 6 | Day of week | 0–7 or SUN–SAT | * ? , - / nL n#k |
L means the last day of the month; L-2 is two days before it. 15W is the weekday nearest the 15th (it never crosses into another month). In the day-of-week field FRIL or 5L is the last Friday, and MON#2 is the second Monday.
Common Spring cron examples
| Expression | Meaning |
|---|---|
| */30 * * * * * | Every 30 seconds |
| 0 * * * * * | Every minute |
| 0 */5 * * * * | Every 5 minutes |
| 0 0 * * * * | At minute 0 of every hour |
| 0 0 */2 * * * | At minute 0, every 2 hours |
| 0 0 9 * * * | At 09:00 |
| 0 0 9 * * MON-FRI | At 09:00, Monday through Friday |
| 0 0 9,18 * * * | At 09:00 and 18:00 |
| 0 0/30 9-18 * * MON-FRI | Every 30 minutes, every hour from 09:00 through 18:00, Monday through Friday |
| 0 0 0 * * SUN | At 00:00, on Sunday |
| 0 0 0 1 * * | At 00:00, on day 1 of the month |
| 0 0 23 L * * | At 23:00, on the last day of the month |
| 0 0 10 * * MON#1 | At 10:00, on the 1st Monday of the month |
| 0 0 18 * * FRIL | At 18:00, on the last Friday of the month |
| 0 0 9 15W * * | At 09:00, on the weekday nearest day 15 of the month |
| 0 0 0 1 JAN * | At 00:00, on day 1 of the month, in January |
Using the expression with @Scheduled
Enable scheduling once with @EnableScheduling on a configuration class, then put the expression on a no-argument method of a Spring bean. Keep the expression in configuration rather than in code so each environment can change or disable it:
@Component
class ReportJob {
@Scheduled(cron = "${app.jobs.report.cron}", zone = "Asia/Seoul")
void sendDailyReport() {
// ...
}
}
# application.yml
app:
jobs:
report:
cron: "0 0 9 * * MON-FRI" # "-" disables the jobThings that bite in production
- One thread by default.Spring Boot's scheduler pool has a single thread (
spring.task.scheduling.pool.size=1), so a slow job delays every other scheduled method. Raise the pool size or hand long work to an executor. - Every instance runs the job. With three replicas, a daily email goes out three times. Use a lock such as ShedLock, or run scheduled work in a single dedicated instance.
- Time zone drift. Containers default to UTC. Set
zoneexplicitly instead of relying on the host. - Daylight saving time. In zones with DST, 02:30 does not exist on the night clocks jump forward, so the run moves to 03:30. When clocks fall back, the repeated hour is ambiguous. The run list above resolves both cases the way
java.timedoes — or avoid the problem by scheduling in UTC. - Check what is registered. With Spring Boot Actuator,
/actuator/scheduledtaskslists every cron task and its expression — useful when a placeholder did not resolve the way you expected.