← Back to blog
cron-expression-generator

Cron Expression Examples

Complete guide to cron expressions with 20+ annotated examples in bash, Python, and Node.js. Learn all 5 fields, special characters, and common patterns.

cronschedulingdevopsautomation

Cron Expression Examples



30 2 * * 1-5 -- you deployed this expression thinking it runs your backup every weekday at 2:30 AM. It does. But the expression you deployed last week, 0 30 2 * *, ran at minute 0 of hour 30 -- which does not exist, so it never fired at all. Cron syntax looks like someone fell asleep on the keyboard, but it follows a rigid five-field structure that, once internalized, lets you schedule anything from "every 5 minutes" to "the first Monday of each quarter at 6 AM." The problem is that most developers guess instead of learning the fields, and guessing with cron means your job either runs 288 times a day or never runs at all. This guide covers every field, every special character, and 15+ annotated examples you can copy directly into your crontab, Python scheduler, or Node.js application.

The Five Fields: What Each One Controls



Every standard cron expression is exactly five fields separated by spaces. No more, no fewer. Some extended formats (like Quartz for Java) add a sixth field for seconds, but standard Unix crontab, node-cron, and APScheduler all use five.

┌───────────── minute       (0-59)
│ ┌───────────── hour         (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month       (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week  (0-6, Sunday=0, or SUN-SAT)
│ │ │ │ │
* * * * *


Minute (0-59): the minute within the hour. 0 is the top of the hour. 45 is quarter-to. */10 means every 10 minutes (0, 10, 20, 30, 40, 50).

Hour (0-23): uses 24-hour format. 0 is midnight. 13 is 1 PM. 23 is 11 PM. There is no AM/PM notation in cron.

Day of month (1-31): the calendar date. 1 is the first. 15 is the fifteenth. Be cautious with 31 -- February, April, June, September, and November will silently skip that execution.

Month (1-12): 1 is January, 12 is December. Most cron implementations also accept three-letter abbreviations: JAN, FEB, MAR, etc.

Day of week (0-6): 0 is Sunday, 6 is Saturday. Some implementations accept 7 as Sunday as well. Abbreviations work: SUN, MON, TUE, WED, THU, FRI, SAT.

One critical gotcha: if you set both the day-of-month and day-of-week fields to specific values, standard cron uses OR logic. The expression 0 9 15 * 1 does not mean "the 15th if it is a Monday." It means "every 15th of the month AND every Monday" -- two separate triggers combined. This trips up roughly 1 in 3 developers who encounter it for the first time, based on Stack Overflow question frequency.

Special Characters Reference



| Character | Name | Example | What It Does | |---|---|---|---| | * | Wildcard | * * * * * | Matches every value in the field | | , | List | 1,15,30 * * * * | Fires at minute 1, 15, and 30 | | - | Range | 0 9-17 * * * | Fires every hour from 9 AM to 5 PM | | / | Step | */10 * * * * | Fires every 10 minutes starting from 0 | | L | Last (non-standard) | 0 0 L * * | Last day of the month | | W | Weekday (non-standard) | 0 0 15W * * | Nearest weekday to the 15th | | # | Nth day (non-standard) | 0 0 * * 1#1 | First Monday of the month |

The L, W, and # characters are extensions supported by Quartz (Java), Spring, and some cloud schedulers (AWS EventBridge). Standard Linux crontab does not support them. If you need "last day of the month" in standard cron, you use a workaround (shown below).

15+ Annotated Cron Examples



Interval-Based Schedules



# Every minute -- useful for health checks, queue polling
* * * * *

Fires 1,440 times per day. Use with caution.



Every 5 minutes

*/5 * * * *

Fires at :00, :05, :10, :15, :20, :25, :30, :35, :40, :45, :50, :55

= 288 times per day



Every 15 minutes

*/15 * * * *

Fires at :00, :15, :30, :45 = 96 times per day



Every 30 minutes

*/30 * * * *

Fires at :00 and :30 = 48 times per day



Every hour, on the hour

0 * * * *

Fires at 1:00, 2:00, 3:00 ... 23:00, 0:00 = 24 times per day



Every 6 hours

0 */6 * * *

Fires at 0:00, 6:00, 12:00, 18:00 = 4 times per day



Daily Schedules



# Every day at midnight
0 0 * * *

Every day at 2:30 AM (the classic backup time)

30 2 * * *

Twice daily: 9 AM and 5 PM

0 9,17 * * *

Every day at 8:15 AM and 8:15 PM

15 8,20 * * *


Weekday and Weekend Schedules



# Weekdays at 9 AM
0 9 * * 1-5

Monday(1) through Friday(5)



Weekdays at 2:30 AM (database backup, no weekends)

30 2 * * 1-5

Weekends only at noon

0 12 * * 0,6

Sunday(0) and Saturday(6)



Every Monday at 8 AM (weekly report)

0 8 * * 1

Monday, Wednesday, Friday at 6 PM

0 18 * * 1,3,5


Monthly Schedules



# First of every month at midnight
0 0 1 * *

15th of every month at 3 AM

0 3 15 * *

First and 15th (semi-monthly payroll)

0 12 1,15 * *

Last day of the month at midnight (standard cron workaround)

Run daily on days 28-31, but only execute if tomorrow is the 1st

0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /opt/scripts/month-end.sh


Quarterly and Yearly



# First day of each quarter at 6 AM
0 6 1 1,4,7,10 *

Jan 1, Apr 1, Jul 1, Oct 1



Every January 1st at midnight (annual)

0 0 1 1 *


Business Hours Patterns



# Every 15 minutes during business hours on weekdays
*/15 9-17 * * 1-5

Fires from 9:00 to 17:45, Mon-Fri = 36 times per weekday



Every hour during business hours

0 9-17 * * 1-5

9:00, 10:00 ... 17:00 = 9 times per weekday



Every 5 minutes during peak hours (10 AM - 2 PM)

*/5 10-14 * * *

Fires every 5 min from 10:00 to 14:55 = 60 times per day



If you want to build and visualize these expressions interactively, the Cron Expression Generator shows you the next 10 execution times for any expression and exports directly to crontab, Python, or Node.js format.

Implementing Cron Jobs in Bash



The most common deployment target is the system crontab. Here is a production-grade setup with logging, error handling, and monitoring.

#!/bin/bash

/etc/cron.d/database-backup

System cron file for the nightly database backup.



SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin MAILTO=""

Backup at 2:30 AM UTC daily, log output, monitor with CronSafe

30 2 * * * root /opt/scripts/backup-db.sh >> /var/log/backup.log 2>&1


#!/bin/bash

/opt/scripts/backup-db.sh

set -euo pipefail

TIMESTAMP=$(date +%Y%m%d_%H%M%S) DB="production" BACKUP_DIR="/backups/postgres" S3_BUCKET="s3://company-backups/postgres" CRONSAFE="https://ping.cronsafe.luxkern.com/m/abc123"

echo "[${TIMESTAMP}] Starting backup of ${DB}..."

Signal start to CronSafe

curl -fsS "${CRONSAFE}/start" --max-time 10 > /dev/null 2>&1 || true

Dump the database

pg_dump -Fc "${DB}" > "${BACKUP_DIR}/${DB}_${TIMESTAMP}.dump" DUMP_SIZE=$(stat -c%s "${BACKUP_DIR}/${DB}_${TIMESTAMP}.dump" 2>/dev/null || \ stat -f%z "${BACKUP_DIR}/${DB}_${TIMESTAMP}.dump")

Upload to S3

aws s3 cp "${BACKUP_DIR}/${DB}_${TIMESTAMP}.dump" \ "${S3_BUCKET}/${DB}_${TIMESTAMP}.dump"

Clean up local dumps older than 7 days

find "${BACKUP_DIR}" -name "*.dump" -mtime +7 -delete

Signal success with metadata

DURATION=$SECONDS curl -fsS "${CRONSAFE}?duration=${DURATION}&size=${DUMP_SIZE}" \ --max-time 10 > /dev/null 2>&1 || true

echo "[${TIMESTAMP}] Backup complete: ${DUMP_SIZE} bytes in ${DURATION}s"


The MAILTO="" in the cron file suppresses cron's built-in email (which nobody reads). The >> /var/log/backup.log 2>&1 captures both stdout and stderr to a file you can inspect later. The CronSafe ping at the end ensures you know when the job fails -- for a full walkthrough of the dead man's switch pattern, see our guide on how to monitor cron jobs.

Implementing Cron Jobs in Python



Python offers two common approaches: APScheduler for in-process scheduling and python-crontab for managing system crontab entries programmatically.

APScheduler with Cron Triggers



"""
scheduled_tasks.py
In-process scheduler using APScheduler with cron expressions.
"""
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
import httpx
import logging

logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)

CRONSAFE = "https://ping.cronsafe.luxkern.com/m"

scheduler = BlockingScheduler()

def monitored(monitor_id): """Decorator: wraps a job with CronSafe start/success/fail pings.""" def decorator(func): def wrapper(): url = f"{CRONSAFE}/{monitor_id}" try: httpx.get(f"{url}/start", timeout=10) except httpx.HTTPError: pass try: func() try: httpx.get(url, timeout=10) except httpx.HTTPError: pass except Exception as e: logger.error(f"{func.__name__} failed: {e}") try: httpx.get(f"{url}/fail", timeout=10) except httpx.HTTPError: pass raise return wrapper return decorator

@monitored("data_sync") def sync_partner_data(): """Fetch records from partner API and upsert locally.""" resp = httpx.get( "https://api.partner.com/v1/records", headers={"Authorization": "Bearer TOKEN"}, timeout=30, ) resp.raise_for_status() records = resp.json()["data"] logger.info(f"Synced {len(records)} records from partner API")

@monitored("cleanup") def cleanup_temp_files(): """Remove temp files older than 72 hours.""" import os, glob, time threshold = time.time() - (72 * 3600) removed = 0 for f in glob.glob("/tmp/app-cache-*"): if os.path.getmtime(f) < threshold: os.remove(f) removed += 1 logger.info(f"Cleaned up {removed} temp files")

Schedule: sync every 15 min during business hours

scheduler.add_job( sync_partner_data, CronTrigger.from_crontab("*/15 9-17 * * 1-5"), id="sync", max_instances=1, misfire_grace_time=300, )

Schedule: cleanup daily at 3 AM

scheduler.add_job( cleanup_temp_files, CronTrigger.from_crontab("0 3 * * *"), id="cleanup", )

if __name__ == "__main__": logger.info("Scheduler started") scheduler.start()


CronTrigger.from_crontab() accepts the exact same five-field syntax as system cron. The misfire_grace_time=300 parameter gives APScheduler a 5-minute window to run a job if the scheduler was temporarily blocked (e.g., by a long-running previous job). Without this, missed executions are silently skipped.

max_instances=1 prevents overlapping runs. If a sync takes 20 minutes and the next trigger fires at the 15-minute mark, APScheduler skips the second invocation instead of stacking them.

The Five Mistakes That Break Your Schedule



Mistake 1: Swapping minute and hour. 30 2 * * * runs at 2:30 AM. 2 30 * * * tries to run at hour 30, which does not exist -- the job never fires and no error is logged. The minute field comes first. Always.

Mistake 2: Forgetting timezone. Cron expressions carry no timezone information. System crontab uses the server's local timezone. APScheduler and node-cron support explicit timezone configuration. If your server is in UTC and your business hours are CET, 0 9 * * 1-5 fires at 10 AM or 11 AM local time depending on DST. Always set timezone explicitly.

Mistake 3: OR logic on day fields. 0 9 15 * 1 means "the 15th of every month OR every Monday" -- not "the 15th if it falls on Monday." To get AND logic, use a wrapper script:

# Run only on the 15th if it is a Monday
0 9 15 * * [ "$(date +\%u)" = "1" ] && /opt/scripts/report.sh


Mistake 4: Overlapping long-running jobs. A database backup taking 20 minutes, scheduled every 15 minutes, means two backups run simultaneously. Use a lock file:

#!/bin/bash
LOCKFILE="/tmp/backup.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Previous run still active"; exit 0; }

Your job runs here

pg_dump -Fc production > /backups/prod.dump curl -fsS "https://ping.cronsafe.luxkern.com/m/abc123" > /dev/null 2>&1 || true


flock is cleaner than manual lock files because the kernel releases the lock automatically if the process crashes -- no stale lockfile problem.

Mistake 5: Not monitoring the job. Cron does not alert on failure. If your job exits with code 1 at 2 AM, cron writes a line to syslog and moves on. Without a dead man's switch, you find out days later. For detailed guidance on setting up cron job failure alerts with multi-channel escalation, read the companion guide.

Quick Reference Table



| Schedule | Expression | Runs Per Day | |---|---|---| | Every minute | * * * * * | 1,440 | | Every 5 minutes | */5 * * * * | 288 | | Every 15 minutes | */15 * * * * | 96 | | Every hour | 0 * * * * | 24 | | Every 6 hours | 0 */6 * * * | 4 | | Daily at midnight | 0 0 * * * | 1 | | Daily at 2:30 AM | 30 2 * * * | 1 | | Weekdays at 9 AM | 0 9 * * 1-5 | 1 (weekdays) | | Weekends at noon | 0 12 * * 0,6 | 1 (weekends) | | Monday at 8 AM | 0 8 * * 1 | 1 (Mon) | | 1st of month at midnight | 0 0 1 * * | 1 (monthly) | | First day of quarter | 0 6 1 1,4,7,10 * | 1 (quarterly) | | Annual (Jan 1) | 0 0 1 1 * | 1 (yearly) | | Business hours, 15-min intervals | */15 9-17 * * 1-5 | 36 (weekdays) |

Bookmark this table. You will come back to it more often than you expect.

Build It Visually, Monitor It Automatically



If you are still second-guessing whether your expression fires on Tuesday or Thursday, paste it into the Cron Expression Generator. It shows the next 10 execution times, validates the syntax, and exports to crontab, Python, and Node.js formats.

Once your schedule is correct and your jobs are running, add monitoring so you know when they stop. CronSafe takes one curl line per job and alerts you via Slack, email, or SMS when a run is missed. Free for 20 monitors -- no credit card.