Some work on a server can’t wait for a human to remember it. Backups need to run every night. Old log files need clearing every week. A report needs to be emailed every Monday at 8 a.m. You could set an alarm and do it by hand forever — or you could hand the job to cron, the part of Linux whose entire purpose is to run things on a schedule, quietly, without you watching.
Cron is one of those tools that has been around for decades and is still everywhere. Once you understand its one slightly cryptic line — the five-field schedule — you can automate almost any repetitive task on a server. Let’s take it apart piece by piece, starting from zero.
What a cron job actually is
A cron job is a command (or script) plus a schedule that tells the system when to run it. The schedule and the command live together on a single line, and the system stores a list of those lines for you.
Behind the scenes there’s a small background program — a service — called the cron daemon (often shown as cron or crond). It’s part of the family of always-running background programs we covered in processes and services. The daemon wakes up roughly every minute, looks at every scheduled line it knows about, and asks a simple question: is it time to run this one yet? If the answer is yes, it runs the command. If not, it goes back to sleep and checks again a minute later.
That’s the whole idea. Cron doesn’t do the work itself — it’s a patient clock-watcher that launches your commands at your chosen times. The list of those scheduled lines is called a crontab, short for “cron table.”
The daemon has to be running
Cron only fires if the cron service is actually alive on the machine. On most servers it starts automatically at boot and stays up, so you rarely think about it. But if your scheduled jobs mysteriously never run, the very first thing to check is whether the cron daemon is running at all — a stopped service means nothing gets scheduled, no matter how perfect your crontab looks.
Reading the five-field schedule
The famous (and famously confusing) part of cron is the schedule itself. It’s five numbers in a row, each describing one unit of time, from smallest to largest:
┌───────────── minute (0 - 59)
│ ┌─────────── hour (0 - 23)
│ │ ┌───────── day of month (1 - 31)
│ │ │ ┌─────── month (1 - 12)
│ │ │ │ ┌───── day of week (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * * command-to-run
Read it left to right: minute, hour, day-of-month, month, day-of-week, then the command. An asterisk (*) in any field means “every value” — every minute, every hour, and so on.
So the simplest schedule of all is five asterisks:
* * * * * /path/to/script.sh
That reads as “every minute of every hour of every day” — the job runs once a minute, forever. Useful for testing; rarely what you want in production.
Let’s make it concrete. Here are real schedules you’d actually use, with a plain-English reading for each:
# At 02:30 every night — a classic time for backups
30 2 * * * /opt/scripts/backup.sh
# Every 15 minutes, around the clock
*/15 * * * * /opt/scripts/health-check.sh
# At 08:00 every Monday — Monday = day-of-week 1
0 8 * * 1 /opt/scripts/weekly-report.sh
# On the 1st of every month at midnight
0 0 1 * * /opt/scripts/monthly-cleanup.sh
# Every hour, on the hour
0 * * * * /opt/scripts/sync.sh
The trick that unlocks everything: when a field is *, it places no restriction. The job runs whenever the non-asterisk fields all match. In 30 2 * * *, only the minute (30) and hour (2) are pinned, so it matches once a day — at 02:30, every day, every month, regardless of weekday.
The shortcuts: steps, lists, and ranges
You don’t have to write a single number in each slot. Cron understands a few small patterns that make schedules far more expressive:
*/n— a step.*/15in the minute field means “every 15 minutes” (at :00, :15, :30, :45).*/2in the hour field means “every 2 hours.”a,b,c— a list.0 9,12,17 * * *runs at 9 a.m., noon, and 5 p.m. — three specific hours.a-b— a range.0 9-17 * * 1-5runs every hour from 9 a.m. to 5 p.m., Monday through Friday — a “business hours, weekdays” schedule.
You can combine them too. 0 8-18/2 * * * means “every 2 hours between 8 a.m. and 6 p.m.” Read a complicated schedule one field at a time and it always resolves cleanly.
Don't trust your gut — verify the schedule
Cron expressions are easy to misread, and a wrong one can run far too often (or never). Before trusting a schedule on a real server, double-check what it actually means in plain language: say each field out loud, or write the human reading in a comment right next to the line, like the examples above. A schedule that runs every minute when you meant every month can quietly hammer your server for days before anyone notices.
Editing your crontab
Each user on the system has their own personal crontab. You don’t edit the file directly with a text editor — instead you use the crontab command, which opens your list safely and reloads it for you when you’re done.
# Open YOUR crontab for editing
crontab -e
# List (print) your current crontab without editing
crontab -l
# Wipe your entire crontab — careful, no confirmation
crontab -r
The first time you run crontab -e, it may ask which text editor to use; pick whichever you’re comfortable with. Inside, each non-blank, non-comment line is one job. Lines starting with # are comments and ignored — perfect for labeling what each job does.
A clean, well-documented crontab looks like this:
# Nightly database backup at 02:30
30 2 * * * /opt/scripts/backup-db.sh
# Clear temporary files every Sunday at 04:00
0 4 * * 0 /opt/scripts/clean-temp.sh
# Renew certificates twice a day (recommended cadence)
0 0,12 * * * /opt/scripts/renew-certs.sh
There’s also a system crontab at /etc/crontab and small drop-in folders like /etc/cron.d/, plus convenience directories named /etc/cron.daily/, /etc/cron.weekly/, and so on. Anything you place in cron.daily runs once a day without you writing a schedule at all — the system handles the timing. Those are handy for system-wide tasks, but for your own jobs, your personal crontab -e is usually the cleanest place to start.
How a cron job actually runs (and why it surprises people)
Here’s the part that trips up almost everyone the first time. When cron runs your command, it does not run it the same way your interactive shell does. It runs in a stripped-down environment:
your terminal session cron's environment
───────────────────── ───────────────────
full PATH (lots of dirs) tiny PATH (often just /usr/bin:/bin)
your env variables loaded almost no env variables
current dir = wherever you are current dir = your home folder
your shell's config sourced config files NOT sourced
The practical fallout: a script that works perfectly when you type it by hand can silently fail under cron. The two usual culprits are:
- Commands “not found.” Your shell knows where
python,node, orpg_dumplive because yourPATHis rich. Cron’sPATHis bare, so it can’t find them. The fix is to use full absolute paths to every program and file in your job —/usr/bin/python3 /opt/scripts/job.py, not justpython3 job.py. (If you need a refresher on whatPATHeven is, see environment variables.) - Missing environment variables. If your script depends on a variable that’s normally set by your shell login, cron won’t have it. Set what you need explicitly inside the script, or define it at the top of the crontab.
The single most reliable habit: point cron at a script, and make that script set up its own world — absolute paths, its own variables, its own working directory. Then cron only has to launch one thing, and the script takes care of the rest.
Capturing output, or it disappears
By default, anything your job prints — normal output and error messages — gets mailed to the user, if mail is even configured. On a typical server it isn’t, so that output simply vanishes. When a job fails at 3 a.m. and you have no idea why, this is usually the reason: the error existed, but nobody caught it.
The fix is to redirect the output to a log file yourself:
# Send normal output AND errors to a log file
30 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
Two pieces are doing the work here. >> /var/log/backup.log appends the normal output to a log file (the double >> means append, so you keep history instead of overwriting it). The 2>&1 says “send errors to the same place as normal output,” so failures land in the log too. With that one addition, a silent, mysterious job becomes one you can actually debug — just read the log.
A job that never logs is a job you can't trust
Running a scheduled task with no logging is flying blind. You won’t know if it ran, if it succeeded, or if it’s been quietly failing for a month. Always redirect output somewhere you can read it, at least while a new job is young. The few extra characters at the end of the line are the difference between “I can see exactly what happened” and “I have no idea.”
When to reach for systemd timers instead
Cron is wonderful for simple, time-based jobs, and it isn’t going anywhere. But on modern Linux systems there’s a second scheduler worth knowing about: systemd timers. They’re part of the same systemd system that manages services (covered in systemd basics), and they trade a little extra setup for some real advantages:
- Built-in logging. A timer’s output flows into the system journal automatically, so you can read it with the standard journal tools — no manual redirect needed.
- Catch-up runs. A timer can be told to run a job it missed because the server was switched off when it was due. Cron just skips a missed run entirely.
- Dependencies and conditions. A timer can wait until the network is up, or only run if certain conditions hold — things cron can’t express.
The honest rule of thumb: for a quick “run this script every night” task, plain cron is faster to set up and perfectly fine. For something more important — where you care about missed runs, clean logs, or ordering — a systemd timer is the more robust choice. Both have their place; knowing both means you pick the right tool instead of forcing one to do everything.
Why cron is worth learning
Automation is one of the things that separates a server you babysit from a server that takes care of itself. Cron is the oldest, simplest, most universal way to make that happen, and you’ll meet it on practically every Linux machine you ever touch. Backups, cleanups, report generation, certificate renewal, health checks — the unglamorous, essential chores of running a server are very often a cron job behind the scenes.
The five-field schedule looks cryptic for about ten minutes, and then it never confuses you again. Learn to read it, get into the habit of using absolute paths and logging your output, and you’ve got a dependable little robot that does the boring work while you sleep.
Wrapping up
Here’s everything in one place:
- A cron job is a command plus a schedule that tells the system when to run it automatically.
- The cron daemon is a background service that wakes every minute and runs any job whose time has come.
- The schedule has five fields — minute, hour, day-of-month, month, day-of-week — followed by the command.
*means “every value.” - Steps (
*/n), lists (a,b,c), and ranges (a-b) make schedules expressive: every 15 minutes, weekdays 9–5, the 1st of the month, and so on. - Edit your jobs with
crontab -e, list them withcrontab -l. Comment each line so its purpose is obvious. - Cron runs in a bare environment — use absolute paths, set the variables you need, and redirect output to a log (
>> file 2>&1) so failures aren’t invisible. - For missed-run catch-up, automatic logging, and conditions, consider a systemd timer instead.
With scheduled tasks under your belt, you’ve now seen most of the day-to-day toolkit for running a Linux server: navigating it, securing it, moving files onto it, and now teaching it to do its own repetitive work on a clock.