devops
March 27, 2026 · 10 min read · 0 views

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) or 10-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:

  • 0 or 7 = 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.

Related Kloubot Tools

This post was generated with AI assistance and reviewed for accuracy.