A Step-by-Step Guide to Writing Your First Cron Job

Cron jobs have a reputation for being cryptic. You've probably seen someone paste */5 * * * * into a terminal and nodded along like you understood exactly what that meant. The truth is that most developers copy cron expressions from Stack Overflow and pray they work. This tutorial breaks the format apart, field by field, so you actually know what you're writing — and more importantly, why it's not running when you expect it to.

What a Cron Expression Actually Is

A cron expression is a string of five (sometimes six) fields separated by spaces. Each field represents a unit of time, and together they describe a recurring schedule. The classic five-field format looks like this:

minute  hour  day-of-month  month  day-of-week

When cron evaluates whether to run your job, it checks: does the current minute match? Does the current hour match? If all five fields agree with the current time, the job runs. That's the entire logic.

Let's build an expression from scratch — a job that backs up a database every weekday at 2:30 AM. We'll construct it one field at a time.

Field 1: Minute (0–59)

The first field controls which minute within the hour your job fires. We want 2:30 AM, so the minute is 30.

30 * * * *

Right now this runs at the 30-minute mark of every hour of every day. We'll narrow that down as we go.

Useful syntax for this field:

  • * — every minute
  • */15 — every 15 minutes (0, 15, 30, 45)
  • 5,20,50 — at minutes 5, 20, and 50 specifically
  • 0-10 — every minute from 0 through 10

Field 2: Hour (0–23)

Cron uses 24-hour time. 2 AM is simply 2. Update our expression:

30 2 * * *

Now the job runs at 2:30 AM every day. Getting closer.

One thing that trips people up: midnight is 0, not 24. Writing 24 in the hour field will throw a parse error on most systems, or just silently never run. If you mean midnight, use 0.

Field 3: Day of Month (1–31)

For our backup job, we don't want to restrict to specific dates — we want it to run on weekdays, which we'll handle in field 5. So for now, leave field 3 as a wildcard:

30 2 * * *

Where day-of-month becomes interesting is for monthly tasks. Payroll report on the 1st? That's 30 2 1 * *. Last-day-of-month billing? That's where cron gets awkward — most implementations don't support L (last day) without a specialized scheduler like Quartz or a cron-extended platform. Vanilla cron on Linux doesn't know what "last day" means.

Field 4: Month (1–12)

Our backup runs every month, so this stays a wildcard too. But you can also use abbreviated names: JAN, FEB, MAR, etc. If you're scheduling a quarterly report, */3 gives you January, April, July, and October — except it starts from month 1, not from the current month. Verify this against a cron expression tool before assuming it lands on your intended quarter boundaries.

30 2 * * *

Field 5: Day of Week (0–7, where both 0 and 7 = Sunday)

Here's where we filter for weekdays. Monday through Friday is 1-5:

30 2 * * 1-5

That's our complete expression. Every weekday at 2:30 AM.

The Sunday ambiguity (0 or 7) is a genuine source of bugs. Most Linux cron daemons accept both, but some minimal implementations only accept 0. If your job silently skips Sundays, that's your culprit. Stick with 0 for Sunday to be safe.

Installing Your First Crontab Entry

Open your crontab for editing:

crontab -e

This opens the file in your default editor (usually vi or nano). Add this line at the bottom — replacing the script path with your actual backup script:

30 2 * * 1-5 /usr/local/bin/backup-db.sh >> /var/log/backup.log 2>&1

Let's unpack the end of that line. The >> /var/log/backup.log appends stdout to a log file. The 2>&1 redirects stderr to the same place. Without this, any error output gets emailed to the system user — and that mail piles up silently, or gets dropped entirely if mail isn't configured. Always redirect output to a file while you're developing.

Save and exit. Verify the entry was saved:

crontab -l

Verifying Your Expression Before It Runs

Waiting until 2:30 AM to find out your expression was wrong is a bad strategy. There are two fast ways to verify:

Option 1: Use an online cron parser. Tools like crontab.guru let you paste an expression and immediately see a human-readable description plus the next scheduled run times. Paste 30 2 * * 1-5 and confirm it says something like "At 02:30 AM, Monday through Friday." If the description doesn't match your intent, fix the expression now.

Option 2: Run the script directly first. Before relying on cron to call it, run your script manually from the command line as the same user who owns the crontab:

bash /usr/local/bin/backup-db.sh

If it fails here, it'll fail in cron too. Better to know now.

The Gotchas Nobody Warns You About

Environment variables are not what you think

Your shell has PATH, HOME, and a dozen other variables set by your shell profile. Cron doesn't load those. It runs with a minimal environment. This is probably the single most common reason cron jobs fail silently.

Fix it by using absolute paths in your script:

# Instead of this:
pg_dump mydb > backup.sql

# Do this:
/usr/bin/pg_dump mydb > /home/ubuntu/backups/backup.sql

Or export a full PATH at the top of the script:

#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

The day-of-month and day-of-week interaction

If you set both day-of-month and day-of-week to something other than *, cron treats them as an OR condition, not AND. So 30 2 1 * 1 means "run at 2:30 AM on the 1st of each month, OR on every Monday." Not "the first Monday of the month." This surprises almost everyone the first time.

There's no native way to express "first Monday" in standard cron. You'd handle that inside the script with a date check:

# Run script at 2:30 AM on Mondays, but only if it's the first week of the month
30 2 * * 1 [ $(date +\%d) -le 7 ] && /usr/local/bin/monthly-report.sh

Timezone — the silent killer

Cron uses the system timezone by default. If your server runs UTC and you meant 2:30 AM local time (IST, for example), your job is firing at 8:00 AM IST instead of 2:30 AM. Check your server's timezone:

timedatectl

You can set a per-crontab timezone by adding a CRON_TZ line at the top of your crontab:

CRON_TZ=Asia/Kolkata
30 2 * * 1-5 /usr/local/bin/backup-db.sh >> /var/log/backup.log 2>&1

Script permissions

Your script must be executable. If you just created the file, it probably isn't:

chmod +x /usr/local/bin/backup-db.sh

Also verify the script is owned by the same user whose crontab you edited. Root's crontab won't find a script owned only by your regular user if permissions are wrong.

Watching It Run in Real Time

Once your entry is saved, you can watch the cron log to confirm the daemon picked it up:

# On Debian/Ubuntu:
grep CRON /var/log/syslog | tail -20

# On RHEL/CentOS:
tail -f /var/log/cron

When your job fires, you'll see a line like:

Jun 23 02:30:01 myserver CRON[12345]: (ubuntu) CMD (/usr/local/bin/backup-db.sh)

If the entry shows up in the log but the script doesn't do what you expect, the issue is in the script itself — not the cron expression. That's a useful diagnostic split to keep in mind.

Quick Reference: Common Patterns

ExpressionMeaning
* * * * *Every minute
0 * * * *Every hour on the hour
0 0 * * *Every day at midnight
*/10 * * * *Every 10 minutes
0 9 * * 1Every Monday at 9 AM
0 0 1 * *First day of each month at midnight
0 0 * * 0Every Sunday at midnight

Where to Go From Here

Once you're comfortable with the five-field format, the natural next step is to explore cron-adjacent tools that handle scheduling with more reliability: Systemd timers on modern Linux systems offer persistent logging and retry logic that vanilla cron lacks. For application-level scheduling in Python or Node, libraries like APScheduler or node-cron give you the same expression syntax with better error handling and no SSH required.

But for server-side automation — clearing temp files, rotating logs, kicking off nightly builds — nothing beats cron for simplicity. Five fields, one line, and a script that just works. Now you know exactly what those five fields mean.