What Is a Cron Job? The Complete Guide
Cron has scheduled work on Unix systems for fifty years. The syntax takes ten minutes to learn, and the ways it quietly fails take rather longer.
Updated 5 August 2026 · 10 min read
-
Written by
Andrian Valeanu
Founder of Pulsetic
-
Reviewed by
Ionut Caval
Technical reviewer
A cron job is a command that a Unix-like system runs automatically on a fixed schedule. The schedule is written as five fields (minute, hour, day of month, month, day of week) followed by the command, so 0 3 * * * means 3 a.m. every day. Jobs are listed in a file called a crontab, edited with crontab -e.
Key takeaways
- Five fields, in order: minute, hour, day of month, month, day of week. Then the command.
- An asterisk means every value.
*/5means every fifth value, so*/5 * * * *is every five minutes. - One minute is the floor. Cron cannot schedule anything more frequent than that.
- Cron does not read your shell profile, so a job that works in your terminal can still fail. Use absolute paths.
- Cron reports nothing when a job fails, hangs, or never starts. Silence looks identical to success.
What a cron job actually is
Three words get used interchangeably and mean different things. Cron is the background service that has been running on Unix systems since the 1970s. A crontab (cron table) is the file that lists what to run and when. A cron job is one line in that file: a single command on a single schedule.
The mechanism is deliberately simple. The cron daemon wakes once a minute, compares the current time against every scheduled entry it knows about, and runs whatever matches. It does not queue, retry, or check the result. It starts things on time, and that is the whole of its job description.
Typical uses are the unglamorous work that keeps a server healthy: nightly database backups, log rotation, clearing temporary files, sending scheduled reports, warming caches, renewing TLS certificates, and pulling data from an API on a fixed interval.
The five fields
Every crontab line starts with five time fields, then the command. Read left to right:
| Position | Field | Allowed values |
|---|---|---|
| 1 | Minute | 0 to 59 |
| 2 | Hour | 0 to 23 (24-hour clock) |
| 3 | Day of month | 1 to 31 |
| 4 | Month | 1 to 12, or JAN to DEC |
| 5 | Day of week | 0 to 7 (0 and 7 both mean Sunday), or SUN to SAT |
Four operators cover almost everything you will write. An asterisk * means every value. A comma lists values, as in 1,15,30. A hyphen gives a range, as in 9-17. A slash sets a step, so */5 means every fifth value and 0-30/10 means minutes 0, 10, 20 and 30.
Step values (*/5) and name abbreviations (MON) are extensions from Vixie cron rather than the original POSIX specification. Every mainstream Linux distribution supports them. Some minimal systems, notably BusyBox on embedded devices, do not.
Cron schedule examples
The fastest way to learn the syntax is to read it. Each of these is a complete schedule, ready to paste in front of a command:
| Expression | When it runs |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour, on the hour |
30 2 * * * | Every day at 02:30 |
0 9 * * 1-5 | At 09:00, Monday to Friday |
0 0 1 * * | Midnight on the first of every month |
0 0 * * 0 | Midnight every Sunday |
*/15 9-17 * * 1-5 | Every 15 minutes, 9 a.m. to 5 p.m., weekdays |
0 4 1 1 * | Once a year, 1 January at 04:00 |
If you would rather not count fields by hand, the free cron expression generator builds a schedule from dropdowns, translates any existing expression into plain English, and previews the next several run times.
The shorthand strings
Cron accepts a handful of named schedules in place of all five fields, which are easier to read at a glance:
@hourlyis the same as0 * * * *@dailyor@midnightis the same as0 0 * * *@weeklyis the same as0 0 * * 0@monthlyis the same as0 0 1 * *@yearlyor@annuallyis the same as0 0 1 1 *@rebootruns once when the cron daemon starts, which usually means at boot
How to create a cron job
Your personal crontab is edited through the crontab command rather than by opening a file directly, because that lets cron validate the syntax and reload it for you:
- Run
crontab -e. The first time, it may ask which editor to use. - Add a line with the five time fields, then the command, using absolute paths:
0 3 * * * /usr/bin/php /var/www/app/backup.php - Save and exit. Cron picks the change up immediately, with no restart needed.
- Confirm it is registered with
crontab -l, which lists your jobs.
Avoid crontab -r. It deletes your entire crontab immediately, with no confirmation prompt, and it sits one key away from -e on the keyboard.
Where crontabs live
Jobs can be defined in several places, which is why a job you cannot find may still be running:
- User crontabs, managed with
crontab -eand stored under/var/spool/cron/. These run as that user and have no user column. /etc/crontab, the system-wide file. It has a sixth column naming the user to run as./etc/cron.d/, where packages drop their own job files. Same format as /etc/crontab./etc/cron.hourly,.daily,.weekly,.monthly, directories of scripts run on those intervals, usually by run-parts or anacron.
Build a cron schedule without counting fields
The free cron expression generator writes the schedule for you, decodes any expression into plain English, and previews the next run times.
Why cron jobs fail
A job that runs perfectly when you type it into a terminal and does nothing at all from cron is the single most common cron problem, and it nearly always comes down to one of the following. If you are debugging one right now, the cron job not running checklist works through these in diagnostic order.
The environment is not your shell
Cron runs jobs in a deliberately minimal environment. It does not read .bashrc, .bash_profile or .profile, so none of your aliases, exported variables, or PATH additions exist. PATH is typically just /usr/bin:/bin, which is why a job calling python, node, php or anything installed through a version manager fails with "command not found".
The fix is to be explicit. Use the absolute path to the interpreter and the script (/usr/bin/python3 /opt/app/task.py, not python3 task.py), and set any variables the job needs at the top of the crontab itself.
The working directory is not where you think
Cron starts every job in the owner's home directory, not in the directory the script lives in. Any relative path inside the script resolves from there. Either use absolute paths throughout, or begin the command with a cd: cd /var/www/app && ./task.sh.
The percent sign is not a percent sign
Inside a crontab, % is a special character. The first one ends the command and everything after it is fed to the job as standard input, with each subsequent % becoming a newline. This quietly breaks the most natural thing in the world, dating a filename: date +%Y-%m-%d truncates to date +. Escape every percent sign as \%.
Three fixes solve most cron failures: absolute paths for everything, escape % as \%, and redirect output to a log file so there is something to read when it goes wrong.
Missing final newline
A crontab file must end with a newline. A missing one produces the memorable error "errors in crontab file, can't install", and the last job in the file silently never runs. Editing through crontab -e normally handles this for you; pasting a file into place does not.
Where cron output goes
By default, cron emails anything a job writes to standard output or standard error to the user who owns the crontab. On a modern server that mail almost never arrives, because no mail transfer agent is installed. The log records the attempt with a line reading "(CRON) info (No MTA installed, discarding output)", and your job output is gone.
Redirect it somewhere you can read instead. Appending >> /var/log/mytask.log 2>&1 to the command captures both normal output and errors. To confirm cron at least tried to start a job, check the system log:
| System | Where cron logs go |
|---|---|
| Debian, Ubuntu | /var/log/syslog, or journalctl -u cron |
| RHEL, CentOS, Fedora | /var/log/cron, or journalctl -u crond |
| macOS | log show --predicate 'process == "cron"' |
Read that log carefully, because it answers a specific question and only that one. A line there means cron started the command. It says nothing about whether the command succeeded. Where cron logs are stored covers reading them per distribution, and how to capture job output properly.
The gotcha nobody expects: day of month and day of week
This one is in the manual and still catches experienced people. When both the day-of-month and day-of-week fields are restricted, meaning neither is an asterisk, cron runs the job when either matches, not both.
So 0 0 13 * 5 does not mean "Friday the 13th". It means midnight on the 13th of every month, and midnight every Friday. If you genuinely need both conditions, put the day-of-week test inside the command: 0 0 13 * * [ "$(date +\%u)" = "5" ] && /path/to/script.sh.
Restrict day-of-month and day-of-week together and cron ORs them, it does not AND them. This is the documented behaviour, not a bug, and it is the reason some jobs run far more often than intended.
Cron tells you nothing when a job stops
Everything above concerns jobs that fail loudly enough to investigate. The more expensive problem is the one that makes no noise at all.
Cron has no concept of success. It does not check exit codes, it will not retry, it does not alert, and it has no view of whether a job that started ever finished. A backup script that has been exiting with an error every night for three weeks produces exactly the same silence as one that has worked perfectly. So does a script that hangs on a network call and never returns, a server where the cron daemon is not running, and a crontab that was wiped during a migration.
The consequence is a specific and familiar kind of incident: nobody discovers the backups stopped until somebody needs a backup. The gap is not that cron is unreliable, it is that cron was never designed to report.
Closing the gap with heartbeat monitoring
The standard fix inverts the usual monitoring model. Instead of an external service checking whether your server responds, the job itself checks in when it finishes, and the absence of that check-in is what raises the alarm. This is heartbeat monitoring, sometimes called dead man's switch monitoring.
In practice it is one line appended to the command. The job runs, and on success it pings a unique URL. If the ping does not arrive within the expected window, you get alerted. Because the alert is triggered by silence, it catches every failure mode that ordinary logging misses: the job that errored, the job that hung, the daemon that stopped, and the crontab that no longer exists.
Pulsetic does this as cron job monitoring, alongside the expression generator for writing the schedule in the first place.
Cron reports nothing. If a scheduled job matters, monitor its absence, not just its output: a job that never starts writes no error log to find.
Alternatives to cron
Cron is not the only scheduler, and on some platforms it is no longer the default one. The five-field syntax, however, has outlived the program that introduced it and turns up almost everywhere.
| Option | Where it fits | Uses cron syntax |
|---|---|---|
| systemd timers | Modern Linux. Better logging, dependency handling and randomised delays. | No, its own format |
| Kubernetes CronJob | Scheduled work in a cluster, running as pods. | Yes |
| AWS EventBridge Scheduler | Serverless scheduling on AWS. | Yes, with an extra year field |
| Windows Task Scheduler | The Windows equivalent of cron. | No, GUI and XML |
| anacron | Laptops and desktops that are not always on. Runs missed jobs after boot. | No, interval in days |
| Application schedulers | Laravel, Celery, Quartz, Sidekiq, node-cron and similar. | Usually yes |
One thing none of them change is the reporting problem. A Kubernetes CronJob that fails silently is exactly as invisible as a crontab entry that does, so whichever scheduler you choose, something still has to notice when the work stops happening.
See how Pulsetic's cron job monitoring catches this from the outside, across 15+ locations.
Frequently asked questions
-
What does * * * * * mean in cron?
Every minute of every hour of every day. The five asterisks stand for minute, hour, day of month, month and day of week, and an asterisk means every value for that field.
-
Can a cron job run every 30 seconds?
Not directly. Cron evaluates its schedule once a minute, so one minute is the shortest interval it can express. The usual workarounds are a loop with a sleep inside the script, two jobs offset by 30 seconds, or a systemd timer, which supports sub-minute intervals.
-
Why does my cron job work manually but not in cron?
Almost always the environment. Cron does not read your shell profile, so your PATH and exported variables are missing and commands cannot be found. Use absolute paths for the interpreter and the script, and set any required variables at the top of the crontab.
-
How do I check whether a cron job ran?
Look in the system log: /var/log/syslog on Debian and Ubuntu, /var/log/cron on RHEL and Fedora. That confirms cron started the command but not that it succeeded. For that, redirect the job output to a log file, and use heartbeat monitoring to be alerted when an expected run does not happen at all.
-
Catch the next outage before your visitors do.
2-minute setup · Cancel any time
-
No credit card needed