# What Is a Cron Job? How Cron Works and What It Won't Do

By Dan Ochoa · Published 2026-10-25 · Updated 2026-10-31
Canonical: https://cronradar.com/blog/what-is-a-cron-job

> A cron job is a command the cron daemon runs on a schedule. Learn how cron finds and runs jobs on each Linux distro, and the gaps it leaves for you to fill.

**A cron job is a command that the cron daemon runs automatically on a schedule, such as every night at 02:30 or every five minutes.** Each job is one line in a crontab: five time fields, then the command. The daemon wakes once a minute, starts every line whose fields match the current time, and mails any output to the crontab's owner. That is nearly the whole contract. Cron doesn't retry a failed run, make up a run it missed while the server was down, stop a run that hangs, or tell anyone when a job stops running.

Linux servers run one of three implementations, and this guide names the one behind each behavior: cronie (Fedora, RHEL and its derivatives, Arch), Debian's `cron` package (Debian, Ubuntu), and BusyBox `crond` (Alpine and other BusyBox-based images). Cronie is "based on the original vixie-cron" ([cronie README](https://github.com/cronie-crond/cronie)), and Debian's package is Vixie cron 3.0 with Debian's patches ([source package](https://sources.debian.org/src/cron/3.0pl1-197/)), so the two agree on most things. BusyBox's is a separate, smaller program, a port of dcron ([crond.c](https://github.com/mirror/busybox/blob/1_36_1/miscutils/crond.c)). [A history of cron](https://cronradar.com/blog/history-of-cron) covers where Vixie cron and its descendants came from.

## A Cron Job, Line by Line

```text
# min hour day-of-month month day-of-week command
30 2 * * * /opt/app/backup.sh >> /var/log/app/backup.log 2>&1
```

- `30 2 * * *` is the schedule: minute 30, hour 2, any day of the month, any month, any day of the week. Cron reads it in the server's local time zone. [Cron expression syntax](https://cronradar.com/blog/cron-expression) covers every field and operator.
- `/opt/app/backup.sh` is the command. Everything after the fifth field is handed to the shell as one string.
- `>> /var/log/app/backup.log 2>&1` appends the job's output and errors to a file. Without it, the output is mailed, as described below.

Three words get used interchangeably. _Cron_ is the daemon, named `cron` on Debian and Ubuntu and `crond` on most other systems. A _crontab_ (cron table) is a file of job lines, and `crontab` is also the command that installs per-user crontabs: `crontab -e` edits yours and `crontab -l` prints it. A _cron job_ is one line. [The crontab guide](https://cronradar.com/blog/crontab-guide) covers the command and the files in full.

## How the Cron Daemon Works

### Where It Finds Jobs

Cronie and Debian's cron read three places:

1. **User crontabs** in a spool directory, one file per account, named after the user and installed with `crontab`. The paths are in the distribution table below. Don't edit these files in place.
2. **`/etc/crontab`**, the system crontab. Its lines carry an extra user field between the schedule and the command: `30 2 * * * appuser /opt/app/backup.sh`.
3. **`/etc/cron.d/`**, drop-in files in the same system format, where packages and configuration tools put their jobs. Debian's cron ignores files there whose names contain anything other than letters, digits, underscores, and hyphens, so a file named `backup.cron` never runs ([Debian cron(8)](https://manpages.debian.org/trixie/cron/cron.8.en.html)).

The daemon doesn't read `/etc/cron.hourly`, `cron.daily`, `cron.weekly`, or `cron.monthly` directly. On Debian, lines in `/etc/crontab` run them with `run-parts`, leaving the daily, weekly, and monthly ones to [anacron](https://cronradar.com/blog/anacron) when it is installed. On cronie, `/etc/crontab` is empty by default; `/etc/cron.d/0hourly` runs `cron.hourly`, and a script in that directory starts anacron, which runs the other three ([cronie cron(8)](https://man7.org/linux/man-pages/man8/cron.8.html), [Fedora's cronie package](https://src.fedoraproject.org/rpms/cronie)).

BusyBox `crond` reads only its crontab directory: no `/etc/crontab` and no `/etc/cron.d` ([crond.c](https://github.com/mirror/busybox/blob/1_36_1/miscutils/crond.c); unchanged in this respect through BusyBox 1.38.0). Alpine starts it with `-c /etc/crontabs`, and its default root crontab runs `run-parts` on `/etc/periodic/15min`, `hourly`, `daily`, `weekly`, and `monthly` ([crond options](https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/busybox/crond.confd), [default crontab](https://gitlab.alpinelinux.org/alpine/aports/-/blob/master/main/alpine-baselayout/crontab)).

None of them needs a restart after an edit. Cronie watches its files with inotify and falls back to checking modification times every minute. Debian's cron checks the spool directory's modification time every minute. BusyBox rereads its directory when it changes, and rescans it hourly regardless.

### What Happens Each Minute

Every minute, the daemon compares each loaded line with the current local time. (Cronie also accepts a `CRON_TZ` variable that gives one crontab its own time zone.) For each line that matches, it:

1. **Starts a separate process as the crontab's owner**, or as the user named in a system crontab line. It doesn't wait for one job to finish before starting the next.
2. **Runs the command with `/bin/sh`** on cronie and Debian, not your login shell, unless the crontab sets `SHELL` ([crontab(5)](https://man7.org/linux/man-pages/man5/crontab.5.html)).
3. **Gives it a minimal environment.** `HOME` and `LOGNAME` come from `/etc/passwd`. `PATH` defaults to `/usr/bin:/bin` on Debian's cron ([pathnames.h](https://sources.debian.org/src/cron/3.0pl1-197/pathnames.h/)) and `/usr/bin:/bin:/usr/sbin:/sbin` on cronie ([entry.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/entry.c)). Nothing from `.bashrc` or `.profile` is loaded. You can set variables with `NAME=value` lines above the jobs, but BusyBox crond honors only `MAILTO`, `SHELL`, and `PATH` lines and skips any other.
4. **Mails the output, if there is any**, to the crontab's owner or to the `MAILTO` address. No output means no mail, whatever the exit code ([cronie do_command.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/do_command.c)).

Mail needs a working mail transfer agent on the host. Without one, cronie sends the output to syslog instead ([cron(8)](https://man7.org/linux/man-pages/man8/cron.8.html)), Debian's cron logs `No MTA installed, discarding output` and drops it ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/)), and BusyBox mails only when the crontab sets `MAILTO`. Each start is also written to the system log; [how to know if a cron job ran](https://cronradar.com/blog/how-to-know-if-cron-job-ran) shows where to read it and what it can't prove.

_Figure: Once a minute, cronie and Debian's cron start each line that matches the local time in its own process, without waiting for it to finish. Any output is mailed; without a mail transfer agent, cronie sends it to syslog and Debian's cron discards it._

## Which Cron Your Distribution Runs

Checked October 24, 2026, from each distribution's package sources:

| Distribution     | Implementation                                  | Service                          | User crontabs               | Reads `/etc/crontab` and `/etc/cron.d` |
| ---------------- | ----------------------------------------------- | -------------------------------- | --------------------------- | -------------------------------------- |
| Debian 13        | Debian `cron` 3.0pl1-197                        | `cron`                           | `/var/spool/cron/crontabs/` | Yes                                    |
| Ubuntu 26.04 LTS | Debian's `cron`, 3.0pl1-200ubuntu1              | `cron`                           | `/var/spool/cron/crontabs/` | Yes                                    |
| Fedora 43 and 44 | cronie 1.7.2                                    | `crond`                          | `/var/spool/cron/`          | Yes                                    |
| RHEL 9 and 10    | cronie 1.5.7 and 1.7.0 (CentOS Stream 9 and 10) | `crond`                          | `/var/spool/cron/`          | Yes                                    |
| Arch Linux       | None installed by default; cronie is packaged   | `cronie`, not enabled on install | `/var/spool/cron/`          | Yes                                    |
| Alpine           | BusyBox `crond`                                 | `crond` (OpenRC)                 | `/etc/crontabs/`            | No                                     |

Sources: [Debian](https://packages.debian.org/trixie/cron), [Ubuntu](https://launchpad.net/ubuntu/+source/cron), [Fedora](https://src.fedoraproject.org/rpms/cronie), [CentOS Stream](https://gitlab.com/redhat/centos-stream/rpms/cronie), and the [Arch wiki](https://wiki.archlinux.org/title/Cron), which notes that Arch's base system uses systemd timers instead of cron. If a host has no cron daemon running, its scheduled work may live in systemd timers too. [Cron implementations compared](https://cronradar.com/blog/cron-implementations) shows how to check which cron a host runs and where cronie, Debian's cron, BusyBox crond, fcron, and supercronic differ.

## What Cron Won't Do

Cron's contract fits in one sentence: at this minute, start this command. Each gap below is something people assume cron handles, with what fills it.

### No Retries

A run that exits non-zero has failed, and cron starts the command again only at its next scheduled time. Retries belong inside the job, with a cap and a delay between attempts, or in a scheduler that has them.

### No Catch-Up After Downtime

When the daemon starts, after a reboot, a crash, or a container restart, it begins at the current minute: cronie and Debian's cron set their internal clock to the present on startup ([cronie cron.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/cron.c)). Red Hat's documentation puts it plainly: "A cron job is only executed if the system is running on the scheduled time" ([RHEL 7 guide](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-automating_system_tasks)). A 02:30 backup on a server that was rebooting at 02:30 doesn't happen that night. The only look-back is for clock changes: cronie and Debian run skipped fixed-time jobs after a forward jump of under three hours, such as the start of daylight saving time, and BusyBox does the same for jumps under an hour.

Two tools exist for catch-up. Anacron, included with cronie and packaged separately on Debian, runs jobs whose period is measured in days and keeps a timestamp per job, so a daily job missed overnight runs once the machine is back ([anacron(8)](https://man7.org/linux/man-pages/man8/anacron.8.html)). A systemd timer with `Persistent=true` runs a missed `OnCalendar=` activation as soon as the timer starts again ([systemd.timer(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html), systemd 212 and later).

### No Overlap Prevention

Cronie and Debian's cron start a new copy at every matching minute, whether or not the last one has finished. Their job queue only stops the same line from being queued twice within one minute ([cronie job.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/job.c)). A five-minute job that takes seven minutes ends up with two copies running against the same files and tables. BusyBox crond is the exception: it won't start a line while that line's previous run is still going, and it doesn't make up the skipped run later.

The fix is a lock. `flock -n /var/lib/app/sync.lock /opt/app/sync.sh` runs the command only if it can take the lock immediately, and otherwise exits with status 1 without running it ([flock(1)](https://man7.org/linux/man-pages/man1/flock.1.html), util-linux).

### No Timeouts

A job that hangs on a network call or a database lock runs until something kills it; cron has no maximum-runtime setting. On cronie and Debian, the hung copy keeps running while new ones start beside it. On BusyBox, it blocks every later run of that line. Wrap the command: `timeout 30m /opt/app/backup.sh` sends SIGTERM after 30 minutes, and GNU coreutils' `timeout` exits with status 124 when it does ([timeout(1)](https://man7.org/linux/man-pages/man1/timeout.1.html)).

### Alerts Only by Mail, Only for Output

The output mail is cron's only notification. A job that fails without printing anything sends nothing. A job that prints on every success buries the one mail that matters. A job that never started has no output, so it sends no mail either.

### No Dependencies Between Jobs

Each line is independent. "Run the report after the import finishes" can't be written as two entries; scheduling the report an hour after the import is a guess that breaks the day the import is slow. Chain dependent steps in one entry or script, such as `/opt/app/import.sh && /opt/app/report.sh`, or move them to a tool that models dependencies.

### Nothing Finer Than a Minute

Cron examines its entries once a minute, and its schedule has no seconds field ([crontab(5)](https://man7.org/linux/man-pages/man5/crontab.5.html)). The FAQ below covers the workarounds.

### No Coordination Across Servers

Each daemon reads only its own machine's files. Copy a crontab to three app servers and the job runs three times; keep it on one and the job stops when that server does. Cronie has an opt-in clustering mode: with `crond -c`, hosts share a network-mounted `/var/spool/cron`, and only the host named in `/var/spool/cron/.cron.hostname` runs those crontabs ([cron(8)](https://man7.org/linux/man-pages/man8/cron.8.html)). It picks a host by name. If that host goes down, nothing moves its jobs elsewhere, and `/etc/crontab` and `/etc/cron.d` still run on every host.

## Cron-Like Schedulers and What They Add

Each of these fills some of cron's gaps and leaves others:

| Scheduler                                                                                                                              | Adds over cron                                                                                                                                                                                                                                                                                                                                                                                                | Still needs care                                                                                                                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| systemd timers                                                                                                                         | `Persistent=` catch-up; a service that is still active isn't started again; `TimeoutStartSec=` limits a oneshot service's run, which is unlimited by default ([systemd.service(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html)); `OnFailure=` starts another unit on failure ([systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html)) | One host, like cron; the `OnFailure=` unit that notifies someone is yours to write                                                                                                                                                                                    |
| [Kubernetes CronJob](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/)                                             | `concurrencyPolicy`, `startingDeadlineSeconds`, Job retries (`backoffLimit`) and run limits (`activeDeadlineSeconds`)                                                                                                                                                                                                                                                                                         | Creates a Job "approximately once" per scheduled time, so occasionally two or none; see [Kubernetes CronJob monitoring](https://cronradar.com/blog/kubernetes-cronjob-monitoring)                                                                                                          |
| [GitHub Actions `schedule`](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule) | Hosted runners; UTC by default or an IANA `timezone`                                                                                                                                                                                                                                                                                                                                                          | At most every 5 minutes; runs can be delayed or dropped under load; disabled after 60 days without activity in public repositories (checked October 24, 2026)                                                                                                         |
| Cloud schedulers                                                                                                                       | AWS EventBridge Scheduler: a [retry policy](https://docs.aws.amazon.com/scheduler/latest/APIReference/API_RetryPolicy.html) and an SQS [dead-letter queue](https://docs.aws.amazon.com/scheduler/latest/UserGuide/configuring-schedule-dlq.html) for failed invocations; Google Cloud Scheduler: retries with backoff                                                                                         | Cloud Scheduler retries are off by default (`retryCount` 0, at most 5; [checked October 24, 2026](https://docs.cloud.google.com/scheduler/docs/configuring/retry-jobs)); a retry repeats the call, so make the target safe to call twice                              |
| In-app schedulers                                                                                                                      | Hangfire retries a failed job 10 times by default ([docs](https://docs.hangfire.io/en/latest/background-processing/dealing-with-exceptions.html)); Laravel offers `withoutOverlapping()` and `onOneServer()` ([docs](https://laravel.com/docs/13.x/scheduling))                                                                                                                                               | Run only while the app's scheduler process does; Celery needs exactly one beat scheduler per schedule, or tasks run twice ([docs](https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html)); Laravel's scheduler is itself a cron entry running every minute |

A timer's `AccuracySec=` defaults to one minute, so systemd timers aren't more precise than cron until you lower it ([systemd.timer(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html)).

## Why "It Silently Stopped Running" Is the Classic Cron Failure

Every gap above fails the same way: quietly. Cron reports only by mailing output from a job that ran, so the failures that matter most produce nothing:

- The daemon stopped, or a container restarted without it.
- A deploy or configuration-management run overwrote the crontab.
- The server was replaced, and the new one never got the entry.
- The host was down or rebooting at the scheduled minute.
- The job hangs, and on BusyBox every later run waits behind it.
- The job fails without printing, or its mail goes to a mailbox nobody reads.

Each one leaves an absence: no output, no mail, often no log line. The longer the interval, the longer the absence lasts; a monthly job can be broken for a month before anyone needs what it produces. [Why a cron job isn't running](https://cronradar.com/blog/cron-job-not-running) helps once you know something is wrong. The hard part is knowing.

A heartbeat inverts the check. The job reports to an external monitor each time it runs, the monitor knows the schedule, and when an expected report doesn't arrive within a grace period, it alerts. Because the alert comes from a missing signal and the monitor runs somewhere else, it fires even when the server itself is gone. [Dead man's switch monitoring](https://cronradar.com/blog/cron-dead-mans-switch) explains the pattern in full.

### One Wrapper for the Gaps

This CronRadar check-in for a nightly backup also closes the timeout and overlap gaps. There is no CronRadar SDK for crontab and no auto-discovery of crontab entries, so each cron job reports with its own HTTP check-in:

```bash
#!/bin/sh
# /opt/app/nightly-backup-monitored.sh
# The key lives in a file only the job's account can read: cronie and Debian's cron
# copy crontab variables into the headers of the mail they send.
CRONRADAR_API_KEY=$(cat /etc/cronradar/api-key 2>/dev/null)
url="https://cron.life/api/v1/ping/nightly-backup"
run_id="nightly-backup-$(date +%s)-$$"

report() {
  event=$1
  shift
  curl --fail --silent --show-error --max-time 5 --output /dev/null \
    --request POST --get --user "$CRONRADAR_API_KEY:" \
    --data-urlencode "schedule=30 2 * * *" \
    --data-urlencode "gracePeriod=900" \
    --data-urlencode "run_id=$run_id" \
    "$@" "$url/$event" || true
}

report start
timeout -k 1m 30m /opt/app/backup.sh
rc=$?
if [ "$rc" -eq 0 ]; then
  report complete
else
  report fail --data-urlencode "message=backup.sh exited with code $rc"
fi
exit "$rc"
```

Make it executable and schedule it behind a lock:

```text
30 2 * * * flock -n /var/lib/app/nightly-backup.lock /opt/app/nightly-backup-monitored.sh
```

What each piece covers:

- **Missed runs.** If no check-in arrives by 02:30, the monitor goes to `warning`, and to `critical` when the 15-minute grace period ends. That includes the night the server is off.
- **Failures.** A non-zero exit sends `fail`, which alerts immediately, with the exit code in the message.
- **Hangs.** `timeout` stops the job after 30 minutes, and `-k 1m` follows with SIGKILL a minute later if the job ignores SIGTERM. Either way the wrapper sees a non-zero status (124 when SIGTERM ends the job), which reaches CronRadar as a `fail` the same night. Without the timeout, a `start` with no finish is flagged as stuck only when the next scheduled time plus the grace period passes with that `start` still the last event, which for a nightly job is the next night; if the next run's `start` arrives first, it resets that window. CronRadar has no per-job maximum runtime, so the limit belongs in the job.
- **Overlap.** `flock -n` makes any second copy started through the same lock file exit at once instead of running beside the first, so start manual runs the same way. The lock file's directory must be writable by the job's user.
- **Monitoring outages.** `--max-time 5` and `|| true` keep CronRadar out of the job's result: an unreachable monitor can't fail or stall the backup, and the wrapper exits with the job's own code.

Because every check-in carries the schedule, the first run creates the monitor, and a deleted monitor is recreated by the next run. CronRadar reads `schedule` in UTC, so this assumes the server clock is on UTC; otherwise, send the UTC equivalent of the crontab time. BusyBox crond skips the `CRONRADAR_API_KEY=` line, so on Alpine set the key inside the wrapper instead, for example by sourcing a file that only the job's user can read. Alerts go to email, Slack, a webhook, Discord, or PagerDuty.

## Frequently Asked Questions

### What is the difference between cron and crontab?

Cron is the daemon that runs scheduled commands: `cron` on Debian and Ubuntu, `crond` on cronie and BusyBox systems. A crontab is a file of schedule-and-command lines that the daemon reads, and `crontab` is also the command that installs and edits per-user crontabs. A cron job is a single line in a crontab.

### Does cron run missed jobs?

No. If the machine is off or the daemon isn't running at the scheduled minute, that run is skipped for good, and cron carries on from the current minute. Use anacron for daily, weekly, or monthly jobs on machines that aren't always on, or a systemd timer with `Persistent=true`. A heartbeat monitor won't rerun the job, but it tells you the run was missed.

### Can cron run a job every second?

No. Cron's smallest unit is one minute. One workaround pairs `* * * * * /opt/app/poll.sh` with a second line, `* * * * * sleep 30; /opt/app/poll.sh`, for a 30-second rhythm that keeps all of cron's other gaps. For real sub-minute work, run a long-lived worker with its own loop, or use a systemd timer, whose calendar expressions accept seconds ([systemd.time(7)](https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html)) once you lower `AccuracySec=`.

### What user does a cron job run as?

The owner of the crontab it's in. A job in your crontab runs as you, one added with `sudo crontab -e` runs as root, and a line in `/etc/crontab` or `/etc/cron.d/` runs as the user named in its sixth field. It gets that user's `HOME` but not their login environment, so set `PATH` and any other variables the job needs in the crontab or the script.

### Where are cron jobs stored?

Per-user crontabs live in the daemon's spool directory: `/var/spool/cron/crontabs/` on Debian and Ubuntu, `/var/spool/cron/` on cronie systems such as Fedora and RHEL, and `/etc/crontabs/` on Alpine. System jobs live in `/etc/crontab` and `/etc/cron.d/` on cronie and Debian systems. Use `crontab -l` and `crontab -e` to read and edit your own instead of opening the spool file directly.
