The history of Cron and the structural differences between Linux Cron and Quartz were covered in The Complete Guide to Cron Expressions: Everything a Developer Must Know About Scheduling. This post picks up from there. It focuses on the things that trip you up even after you know the syntax — how to combine special characters, which expressions suit which situations, and why the expression you wrote isn't behaving as intended. The goal is to learn Cron Expression practically through real-world examples across system operations, data pipelines, backend services, and CI/CD.

Special Character Combinations: Simpler Than They Look, But Full of Traps

The four basic special characters (*, ,, -, /) are familiar to everyone. The problem is combining them. There are expressions that look like they produce the same result but behave differently.

/ Step: The Starting Point Changes the Result

/ means "repeat every N intervals starting from the start value to the end of the field." */5 is the same as 0/5 — starting from 0 with an interval of 5, it fires at minutes 0, 5, 10, 15, ... But writing 3/5 gives 3, 8, 13, 18, ... — only the starting point differs; the end always fills up to the field's maximum value.

*/5  * * * *   # minutes 0, 5, 10, 15, 20 ... (12 times per hour)
3/5  * * * *   # minutes 3, 8, 13, 18, 23 ... (12 times per hour, but different start)
1/15 * * * *   # minutes 1, 16, 31, 46 (4 times per hour) — may be interpreted as 1-59/15 in some implementations; verify in your environment
0/30 * * * *   # minutes 0, 30 (2 times per hour)
Bash

In practice, when you use */15, you usually want on-the-dot intervals (0, 15, 30, 45 minutes). If you want to avoid the top of the hour for load distribution, intentionally shift the start point like 7/15 (7, 22, 37, 52 minutes).

- and / Combined: Steps Within a Range

Using - and / together means "execute at intervals within a range."

0 9-17/2 * * 1-5   # Mon–Fri, every 2 hours on the hour from 9:00 to 17:00 (9, 11, 13, 15, 17)
0/10 8-18 * * *    # Every day, every 10 minutes between 8:00 and 18:00 (8:00, 8:10, 8:20 ... 18:50)
Bash

One thing to note here: 9-17/2 starts at 9 with an interval of 2, giving 9, 11, 13, 15, 17. If you write 8-18/3 in the same hour field, you get 8, 11, 14, 17 — and 18 is not included (starting from 8 and adding 3 gives 8, 11, 14, 17; the next would be 20, which is outside the range). Whether your intended times are correctly included must be verified by listing them out or using a parser tool.

Mixing , and -: Non-Contiguous Ranges

0 8,12,18 * * *         # On the hour at 8:00, 12:00, 18:00
0 8-10,14-16 * * *      # On the hour at 8, 9, 10, 14, 15, 16
0 0 1,15 * *            # Midnight on the 1st and 15th of each month
0 0 * * 1,3,5           # Midnight on Monday, Wednesday, Friday
Bash

The pattern of mixing ranges and lists like 8-10,14-16 is commonly used when scheduling for business hours. Group the morning block and afternoon block as separate ranges and connect them with ,.

Cron Expression special character combination execution time timeline — comparison of execution times for */5, 3/5, 9-17/2, 8-10,14-16 patterns

Execution times that vary by special character combination: even at the same interval, results differ based on start point and range

Quartz-Exclusive Special Characters: L, W,

Quartz supports additional special characters not found in Linux Cron. The Quartz official documentation has detailed specs, but here are the essentials:

L (Last) — Can be used in the day-of-month and day-of-week fields.

# L in day-of-month: the last day of the month
0 0 0 L * ?        # Midnight on the last day of every month

# L-n in day-of-month: n days before the last day
0 0 0 L-3 * ?      # Midnight 3 days before the last day of the month (e.g., the 28th for a 31-day month, 25th for a 28-day month)

# nL in day-of-week: the last nth weekday of the month
0 0 0 ? * 6L       # Midnight on the last Friday of every month (Quartz: 6=Friday)
Bash

W (Weekday) — Valid only in day-of-month; means the nearest weekday to the specified date.

0 0 9 15W * ?      # 9:00 AM on the nearest weekday to the 15th of each month
                   # If the 15th is Saturday, runs on the 14th (Fri); if Sunday, runs on the 16th (Mon)
Bash

# (Nth weekday) — Valid only in day-of-week; specifies "the nth specific weekday of the month."

0 0 10 ? * 2#1     # 10:00 AM on the first Monday of every month (Quartz: 2=Monday)
0 0 20 ? * 6#3     # 8:00 PM on the third Friday of every month
Bash

The # expression is easy to misuse for "biweekly execution" requirements, but it actually means "the nth occurrence of that weekday," not "the nth week." If the 5th Friday (6#5) doesn't exist in a given month, that execution will be skipped — be careful.

Patterns That Are Easy to Misread

There are expressions that are syntactically correct but behave differently from intuition. Here are the patterns where mistakes tend to repeat.

day-of-month + day-of-week Specified Together: Linux vs Quartz Difference

In Linux Cron, specifying both fields creates an OR condition.

# Linux: midnight on "the 1st of the month OR every Monday"
0 0 1 * 1
Bash

This expression fires on the 1st even if it's not a Monday, and fires on Monday even if it's not the 1st. On months where Monday falls on the 1st, it could trigger twice on the same day (though some implementations deduplicate and run only once). To get "only when it's both the 1st AND a Monday" (AND), Linux Cron cannot express this — you'd need to check the date inside the script.

Quartz resolves this conflict with ?. Setting one of the two to ? makes only the other act as a condition.

# Quartz: only the 1st of each month (regardless of weekday)
0 0 0 1 * ?

# Quartz: only every Monday (regardless of date)
0 0 0 ? * MON
Bash

Two Traps in "Every 30 Minutes"

30 * * * * is correct. But what's the difference between 0/30 * * * * and */30 * * * *? The result is the same — minutes 0 and 30. In most cron implementations the two are treated identically, but depending on the implementation (Quartz, some cloud schedulers, etc.), there can be subtle differences in how the start value is handled. Since 0/30 explicitly communicates the intent of "every 30 minutes starting at minute 0," it is recommended for readability and portability.

Weekday Numbers: Differ by Platform

DayLinux CronQuartzAWS EventBridge
Sunday0 (or 7)1 (SUN)1 (SUN)
Monday12 (MON)2 (MON)
Saturday67 (SAT)7 (SAT)
Range notation0–6 (0=Sun)1–7 (1=Sun) or SUN–SAT1–7 (1=Sun) or SUN–SAT

In Linux, 1-5 means Monday–Friday, but in Quartz, 1-5 means Sunday–Thursday. Copying a weekday expression as-is when switching platforms will silently run on the wrong days. Since Quartz and AWS EventBridge share the same numbering system, using name notation like MON-FRI instead of numbers is the most effective way to reduce mistakes.

The full Quartz weekday number mapping is as follows:

NumberAbbreviationDay
1SUNSunday
2MONMonday
3TUETuesday
4WEDWednesday
5THUThursday
6FRIFriday
7SATSaturday

Real-World Examples by Domain

1. System Operations & Infrastructure

# --- Backups ---
0 2 * * *          # Full DB backup every day at 2:00 AM (most common pattern)
0 2 * * 0          # Weekly full backup (Sunday at 2:00 AM), combined with incremental backups on weekdays
0 3 1 * *          # Monthly archive on the 1st of each month at 3:00 AM

# --- Log Management ---
0 0 * * *          # Log rotation at midnight every day
30 23 * * *        # Compress previous day's logs and upload to S3 at 23:30 daily
0 4 * * 0          # Delete old logs (30+ days) on Sunday at 4:00 AM

# --- System Monitoring ---
*/5 * * * *        # Ping health check endpoint every 5 minutes
* * * * *          # Check process liveness every minute (watchdog) — same as */1, but * is conventional
0 * * * *          # Collect disk usage on the hour every hour
7 * * * *          # Memory/CPU report at 7 minutes past each hour (avoids top-of-hour traffic spike)

# --- Cache Management ---
0 3 * * *          # Full cache warm-up every day at 3:00 AM
*/30 * * * *       # Clean up TTL-expired cache every 30 minutes
Bash

2. Data Pipelines & ETL

# --- Regular Collection ---
0 1 * * *          # Collect external API data every day at 1:00 AM (daily batch)
0 */4 * * *        # Incremental data sync every 4 hours
*/15 9-18 * * 1-5  # Real-time feed collection every 15 minutes during business hours (9–18) on weekdays

# --- Aggregation & Reporting ---
0 6 * * 1-5        # Aggregate previous day's metrics on weekdays at 6:00 AM (to complete before work starts)
0 8 * * 1          # Generate weekly report on Monday at 8:00 AM
0 7 1 * *          # Generate previous month's report on the 1st at 7:00 AM
0 7 1 1 *          # Annual report on January 1st at 7:00 AM

# --- Data Integrity ---
30 2 * * *         # Data integrity check every day at 2:30 AM
0 1 * * 0          # Update data warehouse statistics (ANALYZE) on Sunday at 1:00 AM
0 0 * * * /scripts/month_end_close_if_last_day.sh
                   # Run script at midnight daily — script internally determines if it's the last day
                   # Example script body (Linux GNU coreutils only):
                   #   [ "$(date -d tomorrow +%d)" = "01" ] && ... actual logic
                   # macOS/BSD does not support date -d → use gdate (brew install coreutils)
                   # Recommended to separate into a script file rather than writing conditions inline in crontab
Bash

3. Backend Services

# --- Notifications & Email ---
0 9 * * 1          # Weekly newsletter dispatch on Monday at 9:00 AM
0 9,18 * * 1-5     # Work notifications on weekdays at 9:00 AM and 6:00 PM
0 8 * * *          # Daily to-do reminder at 8:00 AM every day
30 17 * * 5        # Pre-weekend deadline reminder on Friday at 17:30

# --- Session & Token Management ---
0 * * * *          # Clean up expired sessions every hour
*/10 * * * *       # Process refresh queue for soon-to-expire tokens every 10 minutes
0 2 * * *          # Clean up blacklisted token DB every day at 2:00 AM

# --- Payments & Settlement ---
0 0 1 * *          # Process subscription renewals at midnight on the 1st of each month
30 23 * * *        # Daily transaction close settlement at 23:30
0 9 * * 1-5        # Retry previous day's failed payments on weekdays at 9:00 AM
0 0 * * 1          # Generate weekly invoices every Monday at midnight
Bash

4. CI/CD & DevOps

# --- Build & Deploy ---
0 2 * * 1-5        # Nightly build on weekdays at 2:00 AM
0 6 * * 1-5        # Auto-deploy to staging on weekdays at 6:00 AM
0 22 * * 5         # Weekly release deployment on Friday at 22:00 (lowest traffic time)

# --- Test Automation ---
0 1 * * *          # Full regression test nightly at 1:00 AM
0 */6 * * *        # Smoke test every 6 hours
*/30 9-18 * * 1-5  # API integration test every 30 minutes during weekday business hours

# --- Infrastructure Management ---
0 3 * * 0          # Auto-create dependency update PRs on Sunday at 3:00 AM
0 4 1 * *          # Security vulnerability scan on the 1st of each month at 4:00 AM
0 5 * * *          # Clean up Docker images (dangling images) every day at 5:00 AM
30 2 * * 0         # SSL certificate expiration check on Sunday at 2:30 AM
Bash

Domain-specific Cron schedule distribution — 24-hour execution time distribution for System Operations, Data Pipeline, Backend, and CI/CD

Domain-specific schedule distribution: batch and operations tasks are typically concentrated between 1–4 AM, while service tasks are spread throughout business hours

Quartz Real-World Examples: Spring @Scheduled

Here are the most commonly used patterns when using @Scheduled in Spring, paired with their Linux Cron equivalents. Remember that in Quartz, the seconds field comes first.

// Every day at midnight
@Scheduled(cron = "0 0 0 * * ?")       // Linux equivalent: 0 0 * * *

// Weekdays at 9:00 AM
@Scheduled(cron = "0 0 9 ? * MON-FRI") // Linux equivalent: 0 9 * * 1-5

// 1st of every month at 2:00 AM
@Scheduled(cron = "0 0 2 1 * ?")       // Linux equivalent: 0 2 1 * *

// Last day of every month at 23:00 (using L character)
@Scheduled(cron = "0 0 23 L * ?")

// First Monday of every month at 10:00 AM (using # character)
@Scheduled(cron = "0 0 10 ? * MON#1")

// Every 30 seconds (a pattern impossible with Linux Cron)
@Scheduled(cron = "0/30 * * * * ?")

// With timezone (9:00 AM Seoul time)
@Scheduled(cron = "0 0 9 ? * MON-FRI", zone = "Asia/Seoul")
Java

The zone attribute has been supported since Spring 4.0, and is very useful in practice as it allows you to define a schedule based on a specific timezone even when the server runs on UTC. See the Spring official scheduling documentation for more details.

Expression Validation: Pre-Deployment Checklist

Once you've written an expression, verify the following before deployment. The Cron Expression Parser can help you visually verify most of this checklist quickly.

Step 1 — Verify execution times with a parser: Enter the expression into a parser and visually confirm the next 10–20 execution times. It's more common than you'd think to get unexpected times.

Step 2 — Specify the timezone: Recheck execution times in the parser using the actual production timezone. This step is essential if your server runs on UTC but your intended times are in a local timezone.

Step 3 — Check boundary values: Verify how execution times change around month-end (28/29/30/31), and around DST transition dates (US: second Sunday in March, first Sunday in November).

Step 4 — Compare execution interval vs execution time: If there's a chance the job takes longer than its schedule interval, apply flock (Linux) or concurrencyPolicy: Forbid (Kubernetes).

Step 5 — Review load distribution: Check whether all cron jobs on your team are concentrated at a specific time (especially midnight or the top of the hour). Intentionally spreading them out can reduce infrastructure costs.

# Bad example: all jobs concentrated at midnight
0 0 * * * /scripts/backup.sh
0 0 * * * /scripts/report.sh
0 0 * * * /scripts/cleanup.sh

# Good example: intentionally distributed
0  0 * * * /scripts/backup.sh
7  0 * * * /scripts/report.sh
23 0 * * * /scripts/cleanup.sh
Bash

Cron Expression Cheat Sheet

Frequently used patterns in one place. Paste them into the Cron Expression Parser to instantly verify execution times.

DescriptionLinux CronQuartz / Spring
Every minute* * * * *0 * * * * ?
Every 5 minutes*/5 * * * *0 */5 * * * ?
Every 15 minutes*/15 * * * *0 */15 * * * ?
Every 30 minutes0/30 * * * *0 0/30 * * * ?
Every hour on the hour0 * * * *0 0 * * * ?
Every day at midnight0 0 * * *0 0 0 * * ?
Every day at 9:00 AM0 9 * * *0 0 9 * * ?
Weekdays at 9:00 AM0 9 * * 1-50 0 9 ? * MON-FRI
Every Sunday at midnight0 0 * * 00 0 0 ? * SUN
1st of every month at midnight0 0 1 * *0 0 0 1 * ?
Last day of every month at midnightNot possible with expression alone0 0 0 L * ?
Every year on January 1st at midnight0 0 1 1 *0 0 0 1 1 ?
Every hour during weekday business hours0 9-17 * * 1-50 0 9-17 ? * MON-FRI
First Monday of every monthNot possible with expression alone0 0 9 ? * MON#1

Things That Are Impossible with Linux Cron Expressions Alone

The following requirements cannot be implemented with Linux Cron expressions alone. You'll need script conditions or a different scheduler.

RequirementLinux CronWorkaround
Last day of the monthdate -d tomorrow +%d = 01 script condition
nth specific weekdaydate +%W script condition, or Quartz #
Nearest weekdayUse Quartz W character
Every 30 secondsQuartz seconds field, or systemd timer
One-time specific dateat command, or a script that deletes itself
AND condition (date AND weekday)Conditional branching inside script

If you frequently hit these limitations, it may be time to consider Quartz (Java/Spring environments) or systemd timer (Linux system services). The systemd timer's OnCalendar directive allows much richer expressions than Linux Cron, and it integrates dependency management and logging as well.

Closing Thoughts

Cron Expression may seem like a lot to memorize, but it ultimately comes down to repeating patterns. Once you properly understand a few special character combination rules, you can express most schedules — and knowing the workarounds for what you can't express is enough.

Make it a habit to always check execution times with the Cron Expression Parser whenever you write an expression. That moment when you think "it should be right" and move on is, more often than you'd think, where midnight incident alerts begin.

References

  1. Quartz SchedulerCronTrigger Tutorial — Official specification for Quartz-exclusive special characters L, W, #
  2. Spring FrameworkTask Execution and Scheduling — Official documentation for Spring @Scheduled and the zone attribute
  3. Vixie Cron crontab(5) man pagecrontab(5) - Linux man page — Official rules for Linux Cron special characters and weekday numbers
  4. AWSEventBridge Scheduler Cron expressions — AWS EventBridge Cron dialect reference
  5. KubernetesCronJob Documentation — Kubernetes CronJob concurrencyPolicy and timezone configuration documentation
Popular Posts
Everything About ASCII Conversion: How Computers Understand Characters10 February 2026
#Text & Encoding
The Complete Guide to Cron Expressions: Everything a Developer Must Know About Scheduling25 February 2026
#Time & Date
The Mathematical Cause of Gimbal Lock and How to Avoid It: A Complete Guide to Euler Angles vs Quaternions07 March 2026
#Mathematical
Understanding 3D Rotation: A Practical Guide to Quaternions, Euler Angles, and Rotation Matrices21 January 2026
#Mathematical
Everything About Unix Timestamps: A Developer's Guide to Handling Time as Numbers21 February 2026
#Time & Date