Skip to content

How to schedule a job with cron

You have a command that should run on a schedule, for example a report every night at 02:30, and you want cron to run it. This guide makes sure a cron daemon is installed and running, schedules the command as a system job in /etc/cron.d/ (or in your own crontab), tests it with a schedule of every minute, and then sets the real schedule.

The example job appends the disk usage of / to a log file. Replace it with your own command.

On a systemd distribution, a systemd timer is an alternative with better logging and catch-up of missed runs; cron and systemd timers compares the two.

Before you begin

  • You need root access through sudo.
  • The command you schedule runs with a minimal environment: a short PATH, no variables from your shell profile, and /bin/sh as the shell. Use absolute paths in it.

Steps

  1. Make sure a cron daemon is installed, enabled and running. Whether one is installed by default depends on the distribution and on how the system was installed.

    Ubuntu server and cloud images include the cron package (ISC cron); Debian's cloud images and minimal installations don't. Install it if command -v crontab prints nothing, then check the service:

    sudo apt-get update
    sudo apt-get install -y cron
    sudo systemctl enable --now cron.service
    
    $ systemctl is-active cron.service
    active
    

    On Debian, cron recommends a mail transfer agent, so this also installs exim4 unless one is already present: cron mails the output of jobs to their owner. Add --no-install-recommends to install cron alone; job output that isn't redirected is then discarded.

    The cronie package, with cronie-anacron, is installed and running by default. Make sure:

    sudo dnf install -y cronie
    sudo systemctl enable --now crond.service
    
    $ systemctl is-active crond.service
    active
    

    Arch installs no cron daemon by default. Install cronie and enable its service:

    sudo pacman -Syu --noconfirm --needed cronie
    sudo systemctl enable --now cronie.service
    
    $ systemctl is-active cronie.service
    active
    

    Gentoo installs no cron daemon by default. (BusyBox's busybox-crond OpenRC service, which the sys-apps/busybox package installs, isn't enabled; this guide doesn't use it.) Install cronie, add its OpenRC service to the default runlevel and start it:

    sudo emerge --getbinpkg sys-process/cronie
    sudo rc-update add cronie default
    sudo rc-service cronie start
    
    $ rc-service cronie status
     * status: started
    

    Other daemons that provide virtual/cron, such as sys-process/fcron or sys-process/dcron, work too, but their service names, and some of their file formats, differ. With a systemd profile, enable cronie with sudo systemctl enable --now cronie.service instead (systemd profile, not verified).

  2. Create the command the job runs, /usr/local/bin/disk-usage-report:

    #!/bin/sh
    # disk-usage-report: print a timestamp and the disk usage of /.
    set -eu
    date -Iseconds
    df -h /
    
    sudo chmod 0755 /usr/local/bin/disk-usage-report
    

    Run it once by hand, so you know it works before cron runs it:

    $ /usr/local/bin/disk-usage-report
    2026-11-07T14:20:03+00:00
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/vda1       9.7G  1.9G  7.4G  21% /
    
  3. Schedule it as a system job, first with a schedule of every minute so you can see it run. Create /etc/cron.d/disk-usage-report:

    # Disk usage report. Test schedule: every minute.
    SHELL=/bin/sh
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    MAILTO=""
    * * * * * root /usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1
    
    sudo chmod 0644 /etc/cron.d/disk-usage-report
    
    • Files in /etc/cron.d/ have a sixth field, before the command, naming the user the job runs as (root here).
    • >> … 2>&1 appends both output and errors to a log file, so you can see what the job did.
    • MAILTO="" stops cron from trying to mail the output, since it's redirected anyway.
    • The file name must contain only letters, digits, hyphens and underscores: cron ignores files in /etc/cron.d/ with a dot in the name (such as report.cron) on Debian and Ubuntu.
    • The file must be owned by root, must not be writable by group or others, and must end with a newline.
    • cron starts the job at every scheduled time, even if the previous run is still going. If a run can take longer than the interval, make the command take a lock, as in How to write a safe shell script (step 6).

    cron notices new and changed files in /etc/cron.d/ by itself; there's nothing to reload.

  4. Wait until the next minute has passed, then check that the job ran:

    $ sleep 65
    $ cat /var/log/disk-usage-report.log
    2026-11-07T14:22:01+00:00
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/vda1       9.7G  1.9G  7.4G  21% /
    

    cron also logs each job it starts:

    $ sudo journalctl -u cron.service -n 20 --no-pager | grep disk-usage-report
    Nov 07 14:22:01 server1 CRON[2016]: (root) CMD (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    
    $ sudo journalctl -u crond.service -n 20 --no-pager | grep disk-usage-report
    Nov 07 14:22:01 server1 CROND[5036]: (root) CMD (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    Nov 07 14:22:01 server1 CROND[5034]: (root) CMDEND (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    

    The same lines are written to /var/log/cron.

    $ sudo journalctl -u cronie.service -n 20 --no-pager | grep disk-usage-report
    Nov 07 14:22:00 server1 CROND[1244]: (root) CMD (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    Nov 07 14:22:00 server1 CROND[1243]: (root) CMDEND (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    

    With sysklogd's default configuration, cron's messages go to /var/log/syslog:

    $ sudo grep disk-usage-report /var/log/syslog
    Nov  7 14:22:00 server1 CROND[4685]: (root) CMD (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    Nov  7 14:22:00 server1 CROND[4684]: (root) CMDEND (/usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1)
    
  5. Set the real schedule. Replace the five * fields with the times you want, for example every day at 02:30:

    sudo sed -i 's|^\* \* \* \* \* root|30 2 * * * root|; s|Test schedule: every minute|Every day at 02:30|' /etc/cron.d/disk-usage-report
    
    $ grep root /etc/cron.d/disk-usage-report
    30 2 * * * root /usr/local/bin/disk-usage-report >> /var/log/disk-usage-report.log 2>&1
    

    The fields are minute, hour, day of month, month and day of week; crontab syntax lists the values and shorthands such as @daily. Times are in the system's time zone (timedatectl or date shows it).

Schedule a job as your own user

For a job that should run as you rather than as root, use your personal crontab, which has no user field.

Any user may have a crontab unless /etc/cron.allow or /etc/cron.deny says otherwise.

Any user not listed in /etc/cron.deny (empty by default) may have a crontab.

Any user not listed in /etc/cron.deny may have a crontab.

The crontab spool directory is accessible only to members of the cron group, so add yourself to it, then log out and in again for the change to take effect:

sudo usermod -aG cron "$USER"

Until you do, crontab fails with '/var/spool/cron/crontabs' is not a directory, bailing out.

Edit your crontab with crontab -e, which opens it in the editor named by VISUAL or EDITOR, and installs it when you save and quit. Add a line such as:

*/15 * * * * /usr/local/bin/disk-usage-report >> "$HOME/disk-usage-report.log" 2>&1

To add the same line from a script, without an editor, keep the existing entries and append the new one (|| true keeps a script running under set -e when you have no crontab yet):

{ crontab -l 2>/dev/null || true; echo '*/15 * * * * /usr/local/bin/disk-usage-report >> "$HOME/disk-usage-report.log" 2>&1'; } | crontab -

List it:

$ crontab -l
*/15 * * * * /usr/local/bin/disk-usage-report >> "$HOME/disk-usage-report.log" 2>&1

Test a user job the same way as a system job: temporarily use * * * * *, wait a minute, and check its log. crontab -r removes your whole crontab without asking; use crontab -e to remove a single line.

Verify

  • The cron daemon is running and enabled at boot: systemctl is-enabled cron.service (crond.service on RHEL and its rebuilds, cronie.service on Arch), or rc-update show default on Gentoo, which lists cronie.
  • Your job's log file grows at the scheduled times, and cron's log shows a CMD line for each run.

Troubleshooting

The job never runs. Check the file name and permissions in /etc/cron.d/, and look for a message about the file in cron's log:

  • On Debian and Ubuntu (ISC cron), a name with a dot is ignored without a message. A file that is group- or world-writable is ignored with INSECURE MODE (group/other writable).
  • With cronie (RHEL and its rebuilds, Arch, Gentoo), a file that is executable or group- or world-writable is ignored with BAD FILE MODE.
  • A file not owned by root is ignored by both.
  • A missing newline at the end of the last line makes ISC cron ignore the whole file (Missing newline before EOF, this crontab file will be ignored); cronie skips only the last line (missing newline before EOF).

Make sure the cron daemon is running at all: on Debian cloud images and minimal installations, Arch and Gentoo, nothing schedules jobs until you install and start one.

cron logs the job, but it fails or does nothing. The command runs with cron's environment, not your shell's. Look at the log file the job writes to: command not found means a missing PATH entry, so set PATH= in the crontab or use absolute paths. Commands that work only in your interactive shell (aliases, functions, variables from ~/.bashrc) aren't available.

A % in the command cuts it short. cron treats an unescaped % as a newline, and passes everything after the first one to the command on standard input. Write \% instead, for example date +\%F, or move the command into a script.

Jobs run at the wrong time. cron uses the system time zone. After changing the time zone, restart the daemon so that it picks up the change (sudo systemctl restart cron.service, crond.service or cronie.service, or sudo rc-service cronie restart).

Output disappears, or mail piles up for root. Without MAILTO="" or a redirection, cron mails a job's output to its owner, or to MAILTO, if a mail transfer agent is installed. cronie without a mail transfer agent sends the output to the system log instead. On Gentoo, app-admin/sudo pulls in nullmailer, which only relays mail to another server: until you name one in /etc/nullmailer/remotes and start the nullmailer service, the mail waits in /var/spool/nullmailer/queue. Redirect output to a file or to logger to keep it where you'll look.