# @reboot in Cron: How It Works and Why It Fails

By Dan Ochoa · Published 2026-10-25 · Updated 2026-10-25
Canonical: https://cronradar.com/blog/cron-reboot

> How @reboot works in cronie, Debian cron, and BusyBox, why @reboot jobs fail at boot or skip daemon restarts, and the systemd units that replace them.

**`@reboot` runs a crontab command once, when the cron daemon starts, which on a normal server happens during boot.** It isn't tied to the system being ready: cronie, Debian's cron, and BusyBox crond run `@reboot` lines right after loading their crontabs, possibly before the network is online, and a marker file in `/run` makes them skip those lines when the daemon is only restarted. Most `@reboot` problems follow from that timing, from cron's bare environment, and from the fact that nothing retries a failed run. For anything that must start reliably at boot, a systemd service is the better tool.

Behavior below comes from the source of cronie 1.7.2 (Fedora, RHEL and derivatives, Arch), Debian's cron 3.0pl1-197 (Debian 13; Ubuntu 24.04 and 26.04 carry the same `@reboot` patch), BusyBox crond 1.37.0 (Alpine 3.24), and the systemd 262 manuals, checked October 25, 2026.

## Writing an @reboot Line

`@reboot` replaces all five time fields. In a user crontab it's followed by the command; in `/etc/crontab` and `/etc/cron.d` files, by the user and then the command:

```text
# crontab -e, as alice
@reboot /home/alice/bin/start-tunnel.sh >> /home/alice/tunnel.log 2>&1

# /etc/cron.d/warm-cache
@reboot root /usr/local/sbin/warm-cache >> /var/log/warm-cache.log 2>&1
```

The redirect matters more than usual, for reasons covered below. cronie's [crontab(5)](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/man/crontab.5#L349) describes the line as "Run once after reboot", and Debian's [crontab(5)](https://manpages.debian.org/trixie/cron/crontab.5.en.html) adds the caveat that matters: startup means when the cron daemon starts, which may be before other system services are up.

## What @reboot Means in Each Implementation

All three daemons load their crontabs and start the `@reboot` jobs immediately, without waiting for anything ([cronie cron.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/cron.c#L341-L345), [Debian cron.c](https://sources.debian.org/src/cron/3.0pl1-197/cron.c/#L147-L150), [BusyBox crond.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/miscutils/crond.c#L1063-L1068)). To tell a boot from a daemon restart, each creates a marker file on its first start and skips `@reboot` lines when the file already exists:

|                             | cronie 1.7.2                                                        | Debian cron 3.0pl1-197                                      | BusyBox crond 1.37.0                                                         |
| --------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Marker file                 | `cron.reboot` in its PID directory, `/run/cron.reboot` with systemd | `/run/crond.reboot`                                         | `crond.reboot` in its PID directory: `/var/run` by default, `/run` on Alpine |
| Logged at boot              | Nothing specific; each job logs a `CMD` line                        | `(CRON) INFO (Running @reboot jobs)`                        | Nothing specific; each job logs a `USER ... cmd` line                        |
| Logged on a daemon restart  | `(CRON) INFO (@reboot jobs will be run at computer's startup.)`     | `(CRON) INFO (Skipping @reboot jobs -- not system startup)` | Nothing                                                                      |
| If the marker can't be made | Logs it, then runs the jobs on every daemon start                   | Logs `DEATH` and the daemon exits                           | Skips the jobs without a message                                             |

Sources: cronie [cron.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/cron.c#L503-L529) and [pathnames.h](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/pathnames.h#L36-L50); Debian [cron.c](https://sources.debian.org/src/cron/3.0pl1-197/cron.c/#L282-L313) and [pathnames.h](https://sources.debian.org/src/cron/3.0pl1-197/pathnames.h/#L81-L83); BusyBox [crond.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/miscutils/crond.c#L931-L940) and [Alpine's build config](https://gitlab.alpinelinux.org/alpine/aports/-/blob/3.24-stable/main/busybox/busyboxconfig). BusyBox 1.38.0 has the same code.

The marker works because `/run` is a tmpfs that "is flushed on boot", and `/var/run` is a compatibility symlink to it ([UAPI file hierarchy](https://uapi-group.org/specifications/specs/linux_file_system_hierarchy/)). So `@reboot` means once per boot: neither `systemctl restart cron` nor systemd restarting the daemon after a crash (both units set `Restart=on-failure`) runs those lines again.

systemd itself has no crontab and no `@reboot`. The optional systemd-cron package, which turns crontabs into systemd units, converts an `@reboot` line into a timer with `OnBootSec=` set from the crontab's `DELAY` variable in minutes, one minute at least ([generator, v2.8.2](https://github.com/systemd-cron/systemd-cron/blob/v2.8.2/src/bin/systemd-crontab-generator.cpp#L310-L314), [L635-L638](https://github.com/systemd-cron/systemd-cron/blob/v2.8.2/src/bin/systemd-crontab-generator.cpp#L635-L638)).

## Why @reboot Jobs Fail at Boot

### Cron Doesn't Wait for the Network

The cron daemon is an ordinary boot service. Fedora installs cronie's unit as `crond.service` and Arch as `cronie.service` ([PKGBUILD](https://gitlab.archlinux.org/archlinux/packaging/packages/cronie/-/blob/main/PKGBUILD)), with `After=auditd.service nss-user-lookup.target systemd-user-sessions.service time-sync.target ypbind.service autofs.service` ([cronie.systemd](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/contrib/cronie.systemd), [Fedora spec](https://src.fedoraproject.org/rpms/cronie/blob/rawhide/f/cronie.spec)). Debian's and Ubuntu's `cron.service` has `After=remote-fs.target nss-user-lookup.target` ([cron.service](https://sources.debian.org/src/cron/3.0pl1-197/debian/cron.service/)). Neither names `network-online.target`, the target for "units that strictly require a configured network connection" ([systemd.special(7)](https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html)). `network.target` wouldn't help: "whether any network interfaces are already configured when it is reached is not defined" ([Running Services After the Network Is Up](https://systemd.io/NETWORK_ONLINE/)).

So an `@reboot` job that calls an API, runs `git pull`, or opens a tunnel can start before DHCP or DNS is ready, fail once, and stay failed until the next boot.

Network file systems in `/etc/fstab` are covered by accident. Each such mount is ordered after `network-online.target` and before `remote-fs.target` ([systemd.mount(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html)), and both daemons start after `remote-fs.target`: Debian's directly, cronie's through [systemd-user-sessions.service](https://github.com/systemd/systemd/blob/v262.1/units/systemd-user-sessions.service.in). On a host with an NFS or CIFS line in fstab, the network is up by the time `@reboot` jobs run. Without one, nothing makes cron wait; unless something requires it, `network-online.target` "is not pulled into the boot" at all ([Running Services After the Network Is Up](https://systemd.io/NETWORK_ONLINE/)). A mount marked `nofail` is "not ordered before these target units" ([systemd.mount(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html)), so it may not be mounted yet either.

### Mounts and Home Directories That Aren't There Yet

Crontabs live under `/var/spool/cron`, not in the user's home, so a user's `@reboot` line loads even when the home directory isn't available. Then the daemons differ. cronie changes to `$HOME` before running the command, and if it can't, logs `(CRON) ERROR chdir failed (/home/alice): No such file or directory` and drops the job ([security.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/security.c#L261-L270)). Debian's cron ignores a failed change of directory and runs the job from its own working directory, `/var/spool/cron` ([do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L324), [misc.c](https://sources.debian.org/src/cron/3.0pl1-197/misc.c/#L192-L226)), so relative paths resolve somewhere unexpected. If the directory exists but is still an empty mount point, or is encrypted and unlocked only when its owner logs in, both run the job against whatever is there.

### Cron's Environment, Before Anyone Logs In

An `@reboot` job gets the same environment as any other cron job: a short `PATH` (`/usr/bin:/bin` for Debian's cron), no `~/.profile` or `~/.bashrc`, and only the variables cron sets. [Cron environment variables](https://cronradar.com/blog/cron-environment-variables) lists each implementation's defaults. At boot there's also no login session: no desktop and no `DISPLAY`, and no per-user service manager unless lingering is enabled, since [loginctl(1)](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html) spawns one "at boot" only for lingering users. GUI programs and `systemctl --user` have nothing to connect to. Use absolute paths and set what the script needs inside it.

### Output Mail at Boot

cronie and Debian's cron mail a job's output to `MAILTO`, or to the crontab's owner, through `/usr/sbin/sendmail`. With no mail transfer agent (MTA) installed, Debian's cron discards the output and cronie logs it to syslog instead ([cron "No MTA installed"](https://cronradar.com/blog/cron-no-mta-installed)). With one installed, boot timing decides. Postfix's sendmail relies on postdrop "to create a queue file in the maildrop directory" ([sendmail(1)](https://www.postfix.org/sendmail.1.html)), so the message waits there for Postfix. msmtp "reads a mail from standard input and sends it to a predefined SMTP server" ([msmtp manual](https://marlam.de/msmtp/msmtp.html)), which fails if the network isn't up yet. Redirect `@reboot` output to a file so the evidence doesn't depend on mail.

### Nothing Retries a Failed Run

Once an `@reboot` job exits, cron is done with it until the next boot, and restarting the daemon won't rerun it because the marker is still there. To rerun it, run the command yourself, ideally with cron's environment. Deleting the marker and restarting the daemon (`rm /run/crond.reboot && systemctl restart cron` on Debian) also works, but it reruns every `@reboot` job on the machine, for every user.

## Check Whether @reboot Ran

Confirm the daemon starts at boot, then read this boot's cron messages. The daemon logs under its own name and each job's line under the name in capitals ([Debian do_command.c](https://sources.debian.org/src/cron/3.0pl1-197/do_command.c/#L145-L154), [cronie do_command.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/do_command.c#L116-L126)), and `journalctl -t` takes several identifiers ([journalctl(1)](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html)):

```bash
# Debian and Ubuntu
systemctl is-enabled cron
journalctl -b -t cron -t CRON
stat -c %y /run/crond.reboot   # when this boot's @reboot jobs started

# Fedora and RHEL (on Arch the unit is cronie.service)
systemctl is-enabled crond
journalctl -b -t crond -t CROND
```

On Debian, a boot that ran an `@reboot` job looks like this:

```text
Oct 25 06:01:12 web1 cron[612]: (CRON) INFO (Running @reboot jobs)
Oct 25 06:01:12 web1 CRON[640]: (alice) CMD (/home/alice/bin/start-tunnel.sh >> /home/alice/tunnel.log 2>&1)
```

cronie logs `(CRON) STARTUP (1.7.2)` among its startup lines, followed by the same kind of `CMD` line. A `CMD` line means cron started the command, not that it succeeded; the job's own log has the result. `-b` covers the current boot only, and earlier boots are there only if the journal is persistent. On Alpine, BusyBox crond logs `USER root pid 2301 cmd ...` to syslog for each job it starts ([crond.c](https://github.com/vda-linux/busybox_mirror/blob/1_37_0/miscutils/crond.c#L959-L960)).

## The sleep Hack and a Sturdier Wait

The usual workaround is a fixed delay:

```text
@reboot sleep 60 && /opt/app/bin/sync-feeds
```

It's a guess. A boot that waits on a slow DHCP server, a file system check, or a sluggish NFS server can take longer than 60 seconds, and then the job fails exactly as before; a fast boot wastes the minute. The delay checks nothing, and when the job fails there's no second attempt.

If the job has to stay in cron, wait for the thing it needs, with a deadline, and log the outcome. This script waits up to five minutes for its server's name to resolve:

```bash
#!/bin/sh
# /usr/local/bin/sync-feeds-at-boot
deadline=$(( $(date +%s) + 300 ))
until getent hosts feeds.example.com > /dev/null; do
  if [ "$(date +%s)" -ge "$deadline" ]; then
    echo "sync-feeds: feeds.example.com did not resolve within 5 minutes" >&2
    exit 1
  fi
  sleep 5
done
exec /opt/app/bin/sync-feeds
```

In root's crontab: `@reboot /usr/local/bin/sync-feeds-at-boot >> /var/log/sync-feeds.log 2>&1`. For a mount, test `mountpoint -q /srv/media` instead. It's still a single attempt per boot, which is why the units below are the better fix.

## Replace @reboot With a systemd Unit

### A Service That Starts at Boot and Stays Up

A long-running process started with `@reboot` (a worker, a tunnel, a dev server) belongs in a service unit:

```ini
# /etc/systemd/system/app-worker.service
[Unit]
Description=App queue worker
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/srv/media

[Service]
Type=exec
User=app
ExecStart=/opt/app/bin/worker
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now app-worker.service
```

Then delete the `@reboot` line so the next boot doesn't start a second copy.

- **`Wants=` and `After=network-online.target`** are what [systemd.special(7)](https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html) asks of units that need the network. "Up" is "defined by the network management software", usually "a configured, routable IP address", and the wait happens only if its wait service, `NetworkManager-wait-online.service` or `systemd-networkd-wait-online.service`, is enabled ([Running Services After the Network Is Up](https://systemd.io/NETWORK_ONLINE/)). It doesn't prove a remote server answers, so keep retries in the worker.
- **`RequiresMountsFor=`** adds `Requires=` and `After=` on every mount needed to reach the path ([systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html)).
- **`Type=exec`**, systemd 240 and later, makes the start fail visibly if the binary can't be executed ([systemd.service(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html)).
- **`Restart=on-failure`** restarts after a non-zero exit, most signals, or a timeout. `RestartSec=` defaults to 100ms, and restarts count against the default start limit of five starts in ten seconds ([systemd-system.conf(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd-system.conf.html)), after which systemd stops trying. Five seconds between restarts stays under it.

To hear about crashes, add `OnFailure=` to `[Unit]`, which starts another unit whenever this one enters the failed state; [systemd timers](https://cronradar.com/blog/systemd-timers) shows a notification handler. With the default `RestartMode=normal` a crashing service passes through the failed state on every restart ([systemd.service(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html)), so it fires for each crash, not only when restarts stop.

### A One-Time Task at Each Boot

For a command that runs once and exits, use `Type=oneshot` with `RemainAfterExit=yes`. Without it, "a one-shot service may be started again after exiting successfully, for example as a dependency in another transaction" ([systemd 245 NEWS](https://github.com/systemd/systemd/blob/v262.1/NEWS#L13108-L13114)):

```ini
# /etc/systemd/system/warm-cache.service
[Unit]
Description=Warm the application cache once per boot
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
User=app
ExecStart=/opt/app/bin/warm-cache
TimeoutStartSec=15min

[Install]
WantedBy=multi-user.target
```

`systemctl status warm-cache` then shows `active (exited)` after a successful run this boot, or `failed`. A oneshot has no start timeout by default, hence `TimeoutStartSec=`. Because a target is ordered after the units it wants ([systemd.target(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.target.html)), a slow oneshot also delays the point where `multi-user.target` is reached; move long work to a timer.

For a per-user process, write a user unit with `WantedBy=default.target` and run `loginctl enable-linger alice` so the user's manager starts at boot; the systemd timers guide covers lingering too.

### Recurring Work That Starts After Boot

When `@reboot` is the first of many runs, a timer replaces it and the repeating schedule together:

```ini
# /etc/systemd/system/sync-feeds.timer
[Unit]
Description=Sync feeds two minutes after boot, then every 15 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=15min

[Install]
WantedBy=timers.target
```

The timer starts `sync-feeds.service`, a oneshot like the one above minus two parts. Drop `RemainAfterExit=yes`: a timer doesn't restart a unit that's still active, so the service would run only once. Drop the `[Install]` section too, since the timer is what you enable. `OnUnitActiveSec=` counts from the service's last start, and an `OnBootSec=` time that has already passed when the timer is activated "will immediately elapse" ([systemd.timer(5)](https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html)). [systemd timers vs cron](https://cronradar.com/blog/systemd-timers-vs-cron) covers moving the rest of a crontab.

## @reboot in Docker Containers

Inside a container, `@reboot` means the moment crond starts in that container, and only if the container runs a cron daemon. The host's `@reboot` has nothing to do with it, and a cron daemon as the container's main process brings its own PID 1 problems ([cron in Docker](https://cronradar.com/blog/cron-in-docker)).

The marker file lives in the container's own `/run`. "When the container is deleted, the writable layer is also deleted" ([Docker storage drivers](https://docs.docker.com/engine/storage/drivers/)), so each `docker run`, each Compose recreate, and each replica runs the `@reboot` jobs again. After a crash, Kubernetes "restarts the container with a clean state" ([Volumes](https://kubernetes.io/docs/concepts/storage/volumes/)), so they run again after every crash there. With `--tmpfs /run`, "when the container stops, the tmpfs mount is removed" ([tmpfs mounts](https://docs.docker.com/engine/storage/tmpfs/)), so every start runs them.

For start-up work in a container, use the entrypoint: run the step, then `exec` the main process so it keeps PID 1:

```bash
#!/bin/sh
# docker-entrypoint.sh
set -e
/app/bin/migrate
exec "$@"
```

## Monitoring Something That Runs at Boot

A schedule-based monitor needs a time when a run is due, and a boot task has none: it runs whenever the machine starts. CronRadar compares check-ins with a five-field cron schedule evaluated in UTC and doesn't accept `@reboot` or any other `@` shortcut, so it isn't the tool for an `@reboot` job itself.

For a boot-time oneshot, systemd's state is the signal: a failed run stays `failed` in `systemctl --failed`, and `OnFailure=` can notify you. For a long-running service, `Restart=` recovers crashes and `OnFailure=` reports them. If you also want an outside check that the service keeps working, including when the host never comes back, have the service send a heartbeat at fixed clock times and monitor that heartbeat on a schedule such as `*/5 * * * *`. Send it on the schedule's marks, not every five minutes counted from the service's start: each check-in moves the expected time to the schedule's next occurrence, so a heartbeat that drifts off the marks shows as late every cycle, and as `critical` once the drift is past the grace period and at least two minutes. There's no CronRadar SDK for systemd units or shell scripts and no auto-discovery for them; each heartbeat is an HTTP request, as [how to monitor cron jobs](https://cronradar.com/automation-monitoring) shows.

## Frequently Asked Questions

### Why is my @reboot cron job not running?

Check that the cron daemon is enabled (`systemctl is-enabled cron`, or `crond` on Fedora and RHEL), then look in this boot's journal for the job's `CMD` line. If the line is there, the job started and failed, usually because the network, a mount, or a command on its `PATH` wasn't available yet; redirect its output to a file to see the error. If it isn't, remember that `@reboot` lines run only at the daemon's first start after boot, not when you add the line or restart cron.

### Does @reboot run when cron restarts?

No, not in cronie, Debian's cron, or BusyBox crond. Each creates a marker file in `/run` on its first start and skips `@reboot` lines while that file exists, and `/run` is emptied at boot. Containers behave differently: a new container has no marker, so it runs the `@reboot` jobs again.

### How do I delay an @reboot job until the network is up?

In cron, have the job wait for what it needs, such as its server's name resolving, in a loop with a deadline instead of a fixed `sleep`. The reliable fix is a systemd service with `Wants=network-online.target` and `After=network-online.target`, which systemd holds until the network manager reports the network as up. Keep a retry in the job either way, since "up" usually means an address is configured, not that a remote service answers.

### Does @reboot work in Docker?

Only if a cron daemon runs inside the container, and then it fires when that daemon starts, not when the host boots. Every new container runs the `@reboot` jobs again because it starts without the marker file. For start-up work, run the command in the image's entrypoint before `exec`ing the main process.

### Is systemd better than @reboot?

For anything that must start reliably at boot, yes. A unit can wait for the network and for specific mounts, restart after a crash, log to the journal, and report failures through `OnFailure=`; `@reboot` starts the command once when cron starts and does nothing else. It's still reasonable for a quick, harmless task, or on a system without systemd, such as Alpine with OpenRC.
