Mastering Cron Expressions: A Developer's Guide to Scheduling in CI/CD and Automation
Learn cron syntax inside and out. Build reliable schedules for CI/CD pipelines, background jobs, and automation with practical examples and common gotchas.
Understanding Cron: Why It Matters
Cron expressions are the backbone of scheduled automation. Whether you’re deploying code at 2 AM, running database backups every Sunday, or triggering nightly tests across thousands of servers, you need to understand cron syntax. Yet many developers copy-paste cron strings without truly understanding them — leading to missed deployments, failed backups, and frustrated teams at 3 AM.
This guide will walk you through cron syntax from first principles, show you real-world CI/CD examples, and help you avoid the pitfalls that catch even experienced DevOps engineers.
The Five-Field Cron Format
At its core, a cron expression is deceptively simple: five space-separated fields representing when a job should run.
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (0 = Sunday)
│ │ │ │ │
│ │ │ │ │
* * * * *
Each field accepts:
-
Specific numbers:
5(exactly 5) -
Wildcards:
*(any value) -
Ranges:
9-17(9 through 17 inclusive) -
Lists:
1,3,5(values 1, 3, and 5) -
Steps:
*/5(every 5th value) or10-50/10(every 10th value from 10 to 50)
Example: Breaking Down a Real Expression
30 2 * * 0
This reads as: “At 2:30 AM, every Sunday.”
-
30— minute 30 -
2— hour 2 (2 AM, 24-hour format) -
*— any day of the month -
*— any month -
0— day 0 (Sunday)
Common Cron Patterns for CI/CD
Deployment Schedules
Weekday mornings at 9 AM (for team availability):
0 9 * * 1-5
Breakdown: minute 0, hour 9, any day, any month, Monday–Friday.
Twice daily: 6 AM and 6 PM:
0 6,18 * * *
The comma-separated hours 6,18 mean “at both 6 and 18 (6 PM).”
Every 30 minutes during business hours (9 AM–5 PM, weekdays):
*/30 9-17 * * 1-5
Breakdown:
-
*/30— every 30 minutes (0, 30) -
9-17— hours 9 through 17 inclusive -
1-5— Monday through Friday
Result: Runs at 9:00, 9:30, 10:00, 10:30, … 17:00 on weekdays.
Backup and Maintenance Windows
Daily backup at 3 AM:
0 3 * * *
Weekly full backup on Sunday at 1 AM:
0 1 * * 0
Monthly backup on the first of each month at midnight:
0 0 1 * *
Quarterly cleanup on the first day of each quarter at 4 AM:
0 4 1 1,4,7,10 *
Months 1 (Jan), 4 (Apr), 7 (Jul), 10 (Oct) = Q1, Q2, Q3, Q4.
Test and Validation Schedules
Run full test suite every 6 hours:
0 */6 * * *
Runs at midnight, 6 AM, noon, 6 PM.
Smoke tests every 15 minutes:
*/15 * * * *
Perfect for monitoring job health. Runs at :00, :15, :30, :45 every hour.
Nightly integration tests at 11 PM (avoid peak hours):
0 23 * * *
Real-World CI/CD Examples
GitHub Actions Workflow
GitHub Actions uses cron syntax (with POSIX extensions) in schedule triggers. Here’s a production example:
name: Nightly Database Maintenance
on:
schedule:
# Run at 2 AM UTC every day
- cron: '0 2 * * *'
# Also run at 2 PM UTC on Fridays for weekly checks
- cron: '0 14 * * 5'
jobs:
maintenance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run cleanup
run: |
./scripts/cleanup-old-logs.sh
./scripts/optimize-indices.sh
Note: GitHub Actions interprets times in UTC. If you’re in EST (UTC-5), 2 AM UTC = 9 PM EST previous day.
GitLab CI Pipeline
GitLab CI uses the same cron format for rules: [schedules]:
stages:
- test
- deploy
nightly-e2e-tests:
stage: test
script:
- npm run test:e2e
only:
- schedules
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: always
deployment-check:
stage: deploy
script:
- ./deploy-health-check.sh
# Custom cron runs Mon-Fri at 9:30 AM
# Set via GitLab UI, but looks like: 30 9 * * 1-5
Jenkins Declarative Pipeline
Jenkins uses H (hash) to avoid thundering herd problems:
pipeline {
agent any
triggers {
// Run at a randomized time within 2-4 AM window
// H = hash of job name for load distribution
cron('H 2-4 * * *')
// Weekly security scan, Tuesday at 1 AM
cron('0 1 * * 2')
}
stages {
stage('Security Scan') {
steps {
sh 'trivy scan --severity HIGH,CRITICAL .'
}
}
stage('Backup') {
steps {
sh './scripts/backup-production.sh'
}
}
}
}
Jenkins’ H feature is invaluable for distributed teams — instead of all jobs running at exactly 0 2 * * *, they spread across a window to prevent server overload.
Advanced Patterns and Gotchas
Avoiding the Sunday/Day-of-Week Confusion
One of the most common mistakes: misunderstanding day-of-week encoding.
Wrong assumption:
0 0 * * 0-6
# Does this mean Sunday-Saturday?
Answer: It depends on your system’s convention. Most cron implementations (Linux, GitHub Actions, Jenkins) use:
-
0or7= Sunday -
1= Monday -
6= Saturday
Some systems (rare) use 1 = Sunday. Always verify your platform’s documentation.
The safest pattern for “weekdays”:
0 9 * * 1-5
Explicitly list Monday (1) through Friday (5).
The Day-of-Month vs. Day-of-Week Intersection
When both day-of-month and day-of-week are restricted (not *), cron uses OR logic, not AND.
0 9 15 * 5
This means: “9 AM on the 15th OR every Friday.” Not “9 AM on Friday the 15th.”
If you want “only 9 AM on Friday the 15th,” you need a conditional in your script:
#!/bin/bash
# Run at 9 AM every day
if [[ $(date +%A) == "Friday" ]] && [[ $(date +%d) == "15" ]]; then
echo "Running special job for Friday the 15th"
# your logic here
fi
Leap Years and Edge Cases
Cron does not understand leap years. Setting a job to run on February 29 won’t work as expected:
0 0 29 2 *
# Runs Feb 29 on leap years, but what about non-leap years?
Handle this in your script:
#!/bin/bash
if [[ $(date +%m) == "02" ]] && [[ $(date +%d) == "29" ]]; then
echo "Leap day detected"
fi
Testing and Validating Cron Expressions
Never deploy a cron expression without verification. Use Kloubot’s Cron Builder to validate syntax and preview execution times.
Step 1: Enter your expression
30 2 * * 0
Step 2: See the next 10 executions
Sunday, January 5, 2025 at 2:30 AM
Sunday, January 12, 2025 at 2:30 AM
Sunday, January 19, 2025 at 2:30 AM
...
This catches timezone issues, day-of-week confusion, and step calculation errors before they impact production.
Timezone Handling in Distributed Systems
Cron runs in the local timezone of the system/container. In CI/CD, this is often UTC. Make this explicit:
Docker: Set TZ Environment Variable
FROM ubuntu:22.04
# Set timezone to US Eastern
ENV TZ=America/New_York
RUN apt-get update && apt-get install -y cron
COPY crontab /etc/cron.d/mycron
RUN chmod 0644 /etc/cron.d/mycron
CMD ["cron", "-f"]
Then your crontab uses Eastern time:
# This runs at 2 AM Eastern, every day
0 2 * * *
Kubernetes CronJob: UTC-Aware YAML
Kubernetes CronJobs always use UTC. To deploy at “2 PM NYC time,” calculate: 2 PM EST = 19:00 UTC (or 20:00 EDT in summer).
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-backup
spec:
# 19:00 UTC = 2 PM EST (winter)
schedule: "0 19 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: backup-user
containers:
- name: backup
image: myapp:latest
command:
- /bin/sh
- -c
- |
echo "Backup running at $(date)"
./backup-db.sh
restartPolicy: OnFailure
Pro tip: For multi-region teams, always document cron times in UTC, then convert locally:
# In your documentation or script header
# Cron runs at 19:00 UTC
# Timezone conversions:
# - 2:00 PM EST (UTC-5)
# - 1:00 PM CST (UTC-6)
# - 12:00 PM MST (UTC-7)
# - 11:00 AM PST (UTC-8)
Common Pitfalls and Debugging
Pitfall 1: Using Single-Digit Minutes/Hours Without Leading Zero
Wrong:
5 9 * * * # This is 9:05, not 5:09
Correct:
05 09 * * * # Explicitly 9:05 AM
Actually, single digits work fine in most cron implementations, but for clarity and to avoid confusion, always use two digits.
Pitfall 2: Assuming Steps Start from Zero
*/5 9-17 * * *
This runs at: 9:00, 9:05, 9:10, … 17:55. It does start at the beginning of the range.
But:
10-50/10 * * * *
This runs at minutes: 10, 20, 30, 40, 50 (every 10th minute within the 10–50 range).
Pitfall 3: Forgetting About Daylight Saving Time
When DST transitions occur, your scheduled job times shift. A 2 AM daily job will run at 1 AM or 3 AM on transition days, depending on direction.
Solution: Use UTC for all cron schedules, then convert in your application if needed.
Pitfall 4: Logs Not Recording Job Execution
Cron silently swallows output. To debug:
#!/bin/bash
# my-job.sh
LOG_FILE="/var/log/my-job.log"
{
echo "Job started at $(date)"
# Your actual job
/usr/local/bin/backup-db.sh
RESULT=$?
echo "Job finished with exit code $RESULT at $(date)"
} >> "$LOG_FILE" 2>&1
Then in crontab:
0 2 * * * /path/to/my-job.sh
Check logs:
tail -f /var/log/my-job.log
Cron in Modern Infrastructure
While traditional cron (via crontab -e) still works, modern platforms offer advantages:
| Platform | Advantages | Downsides |
|---|---|---|
| crontab | Simple, universally available | No built-in retry, poor observability |
| GitHub Actions | Free, integrated with repos, good UI | Limited to GitHub, limited execution time |
| GitLab CI | Powerful rules engine, good logging | Requires GitLab instance |
| Jenkins | Highly customizable, distributed | Steep learning curve, requires infrastructure |
| Kubernetes CronJob | Cloud-native, scalable, self-healing | Requires Kubernetes, verbose YAML |
| AWS EventBridge | Serverless, integrates with 90+ services | AWS-specific, pricing can add up |
| CloudFlare Workers | Cheap, distributed globally | Limited execution time (10s), smaller ecosystem |
For small projects, crontab is fine. For production systems with teams and monitoring requirements, use CI/CD platform integrations.
Practical Checklist for Production Cron Jobs
Before deploying any cron expression:
- [ ] Validate syntax using Kloubot’s Cron Builder
- [ ] Verify timezone — UTC or local? Document it explicitly
- [ ] Check day-of-week logic — does day-of-month AND day-of-week interaction matter?
- [ ] Test execution — run the job manually first with the same user/environment
- [ ] Verify logging — ensure output is captured for debugging
- [ ] Plan for failure — add retry logic or alerting
- [ ] Monitor execution — track in Prometheus, DataDog, CloudWatch, or equivalent
- [ ] Document the “why” — future maintainers need to understand the schedule
- [ ] Test DST transitions — verify behavior on spring-forward and fall-back dates
- [ ] Review access permissions — does the cron user have write access to necessary files/databases?
Quick Reference: Most Common Expressions
# Every minute
* * * * *
# Every hour
0 * * * *
# Every day at midnight
0 0 * * *
# Every weekday at 9 AM
0 9 * * 1-5
# Every Monday at midnight
0 0 * * 1
# First day of month at 2 AM
0 2 1 * *
# Every 6 hours
0 */6 * * *
# Every 30 minutes
*/30 * * * *
# Twice daily at 6 AM and 6 PM
0 6,18 * * *
# Every quarter at 4 AM on the 1st
0 4 1 1,4,7,10 *
Conclusion
Cron expressions are simple yet powerful. They’ve driven automated infrastructure for decades because they work reliably. Master the five-field format, understand day-of-week traps, always validate before deploying, and your scheduling will be bulletproof.
For complex scheduling needs, consider whether a modern CI/CD platform better serves your team. But for anything you deploy, cron expertise remains essential — it’s the lingua franca of system administration.
When building your next automation pipeline, use Kloubot’s Cron Builder to validate expressions and preview execution times. Then document the why behind your schedule so future maintainers understand your choices.
Happy scheduling.