·9 min read·Blog

Cron Syntax: The Parts That Bite You (And How to Test Before Deploying)

Cron expressions look deceptively simple. Five fields, numbers and asterisks. Then your scheduled job runs every minute for a week and your database hits 100% CPU. Here's a field-by-field breakdown, the traps I've personally fallen into, and how to validate a cron expression before it goes anywhere near production.

The incident that made me build a cron parser

The tool I built for this was motivated by a specific mistake. I had a background job that was supposed to run once per hour. I wrote * */1 * * *. What I meant was "every hour." What I wrote was "every minute of every hour," which is just every minute. The job ran 60× more than intended.

The correct expression is 0 * * * *— "at minute 0 of every hour." The distinction matters: the first field is minutes, not hours. I had confused the field order under time pressure.

The five (or six) fields

Standard cron (Unix/Linux cron, cronie) has five fields:

┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–6, 0 = Sunday)
│ │ │ │ │
* * * * *

Some systems (AWS EventBridge, Quartz scheduler in Java) add a sixth field for seconds at the beginning: second minute hour day-of-month month day-of-week. If your cron expression isn't firing when expected, check whether your scheduler uses 5 or 6 fields.

Special characters

  • * — "any value". In the minute field, * means every minute. In the hour field, * means every hour.
  • , — value list. 1,15,30in the minute field means "at minute 1, 15, and 30."
  • - — range. 9-17in the hour field means "from 9 AM to 5 PM inclusive."
  • / — step. */15in the minute field means "every 15 minutes" (0, 15, 30, 45). 0/15 means the same thing.

The five mistakes I see most often

1. Using */1 instead of just *

*/1 means "every 1 unit" — equivalent to *. It does not mean "every 1 hour" if it's in the minute field. It means "every 1 minute." I wrote * */1 * * *thinking it meant "hourly." It runs every minute.

2. Day of week indexing (0 vs 7 for Sunday)

Standard Unix cron accepts 0–6 for Sunday through Saturday. It also accepts 7 as Sunday (so both 0 and 7 = Sunday). But some schedulers only accept 0–6. And some use 1–7 (1 = Monday, 7 = Sunday), which is the ISO 8601 weekday numbering.

If a job should run on Sundays only and you write * * * * 7, it may not fire at all on schedulers that reject 7 as out of range. Write 0for Sunday to be safe.

3. Day of month AND day of week conflict

Most cron implementations OR the day-of-month and day-of-week fields — a job fires if either condition is true. 0 9 1 * 1does not mean "9 AM on the first Monday of the month." It means "9 AM on the 1st of the month OR 9 AM every Monday."

This is one of the most surprising behaviors in cron. If you want "first Monday of the month," you need to handle it in the job itself (check if today is both the correct day of week and within the first 7 days of the month).

4. Timezone blindness

Standard cron runs in the timezone of the system it's installed on. If your server is in UTC (very common on cloud hosts), a job scheduled for 0 9 * * *runs at 9 AM UTC — which is 11 AM in Germany, 4 AM in New York, and midnight in Los Angeles.

AWS EventBridge cron, GitHub Actions scheduled workflows, and most SaaS schedulers also use UTC by default. Document the timezone for every cron expression in comments. Cron expressions without timezone context are a maintenance hazard.

5. Forgetting that cron has no "every N months" syntax

*/3in the month field means "months 3, 6, 9, 12" — not "every 3 months from now." There is no way to express "every 90 days" or "every 3 months from a specific starting month" in standard cron syntax. You either hardcode specific months or handle the logic in the job itself.

Common expressions and what they actually mean

ExpressionWhat it does
0 * * * *Every hour, at minute 0
*/15 * * * *Every 15 minutes (0, 15, 30, 45)
0 9 * * 1-59 AM every weekday (Mon–Fri)
0 0 1 * *Midnight on the 1st of every month
0 0 * * 0Midnight every Sunday
30 6 1,15 * *6:30 AM on the 1st and 15th of every month
0 9-17 * * 1-5Every hour from 9 AM to 5 PM, Monday–Friday

How to test a cron expression before it causes damage

Before deploying any cron job to production:

  1. Use the cron parserto see the next 10 scheduled times in human-readable format. If the next fire time is "in 1 second," you wrote * * * * * accidentally.
  2. Add a comment above every cron expression in your code:
    # Every day at 9 AM UTC (= 11 AM Europe/Paris, 4 AM New York)
    0 9 * * *
    This comment will save the next developer (usually future-you) from reverse-engineering the intent.
  3. Run the job manually once before enabling the schedule. If it's a database job, wrap it in a transaction that you can roll back.
  4. For jobs that run more frequently than daily, test in a staging environment with a looser expression first (e.g., every 5 minutes instead of every 15) to verify the job completes within its window before the next run starts.

Related tools

  • Cron Expression Parser — enter a cron expression and see the next 10 scheduled times, field-by-field explanation, and human-readable description.

Written by Achraf A., founder of TheFreeAITools — built in Morocco. The incident described is real; the job was a data sync script I was running on a DigitalOcean droplet in 2024.

☕ Support Us