Get your free server today! View Plans →
Home Plans Blog About Contact Panel Join Discord
Guides

How to schedule tasks with cron

A clear, practical guide to scheduling jobs with cron. You'll edit your crontab, understand the five time fields, automate a real backup script with logging, and fix the errors that trip people up.

How to schedule tasks with cron

By the end of this guide you'll have cron running real jobs on a Linux server, including a backup script that runs on its own every night and writes to a log file you can check. This is written for anyone who manages a server, a game host, or a small app and wants tasks to run automatically without remembering to do them by hand. You don't need to be a Linux expert, but you should be comfortable opening a terminal.

What cron actually is

Cron is a small program that sits in the background on almost every Linux and Unix system. Its only job is to look at a list of scheduled tasks and run them at the right time. Each user on the machine gets their own list, and that list is called a crontab (a portmanteau of "cron table"). There's also a system wide set of crontabs that the operating system uses for things like log rotation.

You don't usually run cron yourself. It's a service that starts at boot and keeps ticking once a minute, checking whether anything is due. When something is due, it runs it. That's the whole idea. The interesting part is how you describe the schedule.

Your personal crontab is stored in a file under /var/spool/cron (the exact path varies by distribution, for example /var/spool/cron/crontabs/ on Debian and Ubuntu). You should never edit that file directly. There's a proper command for editing it, and we'll get to it next. The system crontab lives at /etc/crontab and there are extra drop in files in /etc/cron.d/, plus the ready made /etc/cron.daily, /etc/cron.weekly and /etc/cron.hourly folders if you just want to drop a script in and forget about it.

Editing your crontab with crontab -e

The command you'll use most is crontab -e. The -e stands for edit. It opens your crontab in a text editor, and when you save and close, cron checks the file for mistakes and loads it. If there's a syntax error it tells you and lets you fix it instead of silently breaking.

# open your own crontab for editing
crontab -e

# list what you currently have, without editing
crontab -l

# remove your entire crontab (be careful, there's no undo)
crontab -r

The first time you run crontab -e it may ask which editor you want. Pick nano if you're not sure, since it's the friendliest. If you ever want to change it later, set the EDITOR variable, for example export EDITOR=nano in your shell profile.

To edit another user's crontab (you'll need to be root for this), add -u:

# edit the crontab belonging to the user 'deploy'
sudo crontab -u deploy -e

The five time fields explained

Every line in a crontab that isn't a comment has the same shape. Five fields describe when to run, then the rest of the line is the command. Here's the layout:

# ┌───────────── minute (0 to 59)
# │ ┌───────────── hour (0 to 23)
# │ │ ┌───────────── day of month (1 to 31)
# │ │ │ ┌───────────── month (1 to 12)
# │ │ │ │ ┌───────────── day of week (0 to 6, where 0 is Sunday)
# │ │ │ │ │
# * * * * * command to run

An asterisk means "every value." So five asterisks means every minute of every hour of every day. Beyond plain numbers and asterisks, you have a few handy operators:

Let's read a few real lines so it clicks. The command here is just a placeholder, so focus on the timing.

# every day at 3:30 in the morning
30 3 * * * /usr/local/bin/backup.sh

# every 5 minutes, all day
*/5 * * * * /usr/local/bin/healthcheck.sh

# at 6 PM on weekdays only (Monday to Friday)
0 18 * * 1-5 /usr/local/bin/report.sh

# at 4 AM every Sunday
0 4 * * 0 /usr/local/bin/weekly-cleanup.sh

# on the 1st of every month at midnight
0 0 1 * * /usr/local/bin/monthly-invoice.sh

One small trap with the day fields. If you set both day of month and day of week to something other than an asterisk, cron treats it as "either one matches," not "both must match." Most of the time you leave one of them as an asterisk, so it rarely bites you, but it's worth knowing.

There are also some shorthand keywords you can use instead of the five fields. They read nicely:

# these two lines do the same thing
0 0 * * *      /usr/local/bin/backup.sh
@daily         /usr/local/bin/backup.sh

# other handy ones
@hourly        /usr/local/bin/healthcheck.sh
@weekly        /usr/local/bin/weekly-cleanup.sh
@reboot        /usr/local/bin/start-on-boot.sh

The @reboot keyword is special. It runs once when the machine boots rather than on a clock, which is useful for starting a process that doesn't have its own service file.

Common schedules you'll actually use

Here's a small reference table of patterns that come up over and over. Keep it nearby until the syntax becomes second nature.

What you wantThe schedule
Every minute* * * * *
Every 5 minutes*/5 * * * *
Every 15 minutes*/15 * * * *
Every hour, on the hour0 * * * *
Every night at 2 AM0 2 * * *
Twice a day (8 AM and 8 PM)0 8,20 * * *
Every Monday at 9 AM0 9 * * 1
First day of the month at midnight0 0 1 * *

If you're ever unsure that a schedule means what you think, write it out in plain English first, then translate each field one at a time. That habit catches most mistakes before they reach the server.

Writing and scheduling a backup script

Cron is only as good as the script it runs. So let's write a real one. This example backs up a game server's world directory into a dated archive, then deletes archives older than 7 days so the disk doesn't fill up. A job that quietly eats all your disk is no fun to debug at 2 AM.

Create the script with nano /usr/local/bin/backup.sh and paste this in:

#!/bin/bash
# Simple dated backup with 7 day retention

# stop on errors and treat unset variables as errors
set -euo pipefail

# what to back up and where to put it
SOURCE_DIR="/home/minecraft/server/world"
BACKUP_DIR="/home/minecraft/backups"
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
ARCHIVE="$BACKUP_DIR/world-$TIMESTAMP.tar.gz"

# make sure the destination exists
mkdir -p "$BACKUP_DIR"

# create the compressed archive
tar -czf "$ARCHIVE" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"

# delete backups older than 7 days
find "$BACKUP_DIR" -name "world-*.tar.gz" -mtime +7 -delete

echo "Backup finished: $ARCHIVE"

A couple of notes on that script. The set -euo pipefail line makes the script stop the moment something goes wrong, instead of carrying on and producing a broken archive. The -mtime +7 in the find command means "modified more than 7 days ago," which is how the cleanup works. Change the source path to match your own setup.

Now make it executable and test it by hand first. Always run a script manually before you trust cron with it.

# make the script runnable
chmod +x /usr/local/bin/backup.sh

# run it once yourself to confirm it works
/usr/local/bin/backup.sh

If that printed "Backup finished" and you can see the archive in the backups folder, you're ready to schedule it. Open your crontab with crontab -e and add this line:

# back up every night at 3:15 AM
15 3 * * * /usr/local/bin/backup.sh >> /home/minecraft/backups/backup.log 2>&1

Save and close. Cron will pick it up immediately. You don't need to restart anything.

Capturing output and errors

That backup line ends with >> /home/minecraft/backups/backup.log 2>&1, and that part matters a lot. By default, anything your job prints just disappears, or worse, cron tries to email it to you and you never see it. Sending output to a log file means you can actually check what happened.

Here's what each piece does:

If you don't care about the normal output and only want to know about failures, send the success output to nowhere and let errors come through:

# discard normal output, keep errors going to the log
0 * * * * /usr/local/bin/healthcheck.sh > /dev/null 2>> /home/minecraft/healthcheck-errors.log

Cron can also email you. At the top of your crontab, set the MAILTO variable, and cron will mail any output from your jobs to that address. This only works if the server has a working mail setup, which many fresh servers do not, so treat it as a bonus rather than your main plan.

# put this at the top of your crontab
MAILTO="[email protected]"

# now any output from this job gets emailed to you
30 3 * * * /usr/local/bin/backup.sh

Set MAILTO="" (an empty value) if you want to switch the emails off entirely.

The PATH gotcha that catches everyone

This is the single most common reason a cron job "doesn't run" when it ran fine in your terminal. Cron runs with a very stripped down environment. The PATH it uses is short, often just /usr/bin:/bin, which is nowhere near the full PATH your login shell has. So a command that works when you type it can fail under cron because cron can't find it.

The fix is simple: use full, absolute paths to everything. Don't write node app.js, write /usr/bin/node /home/user/app/app.js. Find the full path of any command with which:

# find out where a command actually lives
which node
# /usr/bin/node

which python3
# /usr/bin/python3

which pg_dump
# /usr/bin/pg_dump

You can also set a fuller PATH at the top of your crontab so your scripts behave more like they do in a normal shell:

# set this near the top of your crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# now this works without a full path
*/10 * * * * mytool --check

The same warning applies to relative file paths inside your scripts. Cron does not start in your home directory, so a script that reads config.yml from "the current folder" may not find it. Either cd to the right place at the top of the script or use absolute paths throughout. We lean toward absolute paths everywhere, because they remove the guesswork.

Systemd timers, the modern alternative

On most current Linux distributions there's a second way to schedule things called systemd timers. Cron is still everywhere and still perfectly good, but timers have a few advantages worth knowing about. They log to the journal so you can see exactly when a job ran and what it printed, they can run a job that was missed while the machine was off, and they tie into the same service system as the rest of your server.

A timer comes in two small files. One describes the work (a .service file) and one describes the schedule (a .timer file). Here's a backup timer that mirrors our cron example:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly world backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run the world backup every night

[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true

[Install]
WantedBy=timers.target

Then enable and start the timer, and check it's scheduled:

# reload so systemd sees the new files
sudo systemctl daemon-reload

# turn the timer on and start it now
sudo systemctl enable --now backup.timer

# see when it will next fire
systemctl list-timers backup.timer

For a single simple job, cron is quicker to set up. For something important where you want clear logs and missed run handling, a timer is often the better choice. Pick whichever fits.

Troubleshooting

The job didn't run at all. First check that cron is actually running with systemctl status cron (it's called crond on Red Hat family systems). Then confirm your line is really in the crontab with crontab -l. A surprising number of "broken" jobs were simply never saved.

It runs in my terminal but not under cron. This is almost always the PATH problem from earlier. Switch every command and file in the job to a full absolute path, then test again. Reading your log file (the one you set up with 2>&1) will usually show a "command not found" or "no such file" error that points straight at the culprit.

Permission denied. The script needs the executable bit set, so run chmod +x /path/to/script.sh. Also make sure the user the cron job belongs to can actually read the source files and write to the destination. A backup job that can't write to its backup folder will fail every night in silence unless you're logging.

Where can I see cron's own log? On Debian and Ubuntu, look in /var/log/syslog and filter for cron:

# show recent cron activity
grep CRON /var/log/syslog | tail -20

# on systemd based systems you can also use the journal
journalctl -u cron --since "1 hour ago"

If you see your job listed there with the right time but it still didn't do anything, the schedule fired and the problem is inside your script, so go back to your log file. If you don't see it listed at all, the schedule itself is wrong.

The percent sign breaks my command. Inside a crontab, an unescaped % is treated specially (it becomes a newline). If your command uses date with format codes directly in the crontab line, escape each one as \%, or better, move the logic into a script where normal rules apply.

Wrapping up

You now have the pieces that matter: how to edit your crontab safely, how to read and write the five time fields, a working backup script with retention, output logging so you're never guessing, and the absolute path habit that prevents the most common failure. Start with one small job, watch its log for a day or two to confirm it fires when expected, then add more as you trust it. If you outgrow cron's logging, the systemd timer above is an easy step up. Either way, the goal is the same, which is letting the server handle the boring repeated work so you don't have to.

Common questions

How do I edit my crontab?

Run crontab -e to open your personal crontab in a text editor. When you save and close, cron checks the file and loads it. Use crontab -l to view it without editing, and add sudo -u username to edit another user's crontab.

What do the five fields in a cron line mean?

They set the schedule, in order: minute (0 to 59), hour (0 to 23), day of month (1 to 31), month (1 to 12), and day of week (0 to 6, Sunday is 0). An asterisk means every value, a comma lists values, a dash sets a range, and a slash sets a step like every five minutes.

Why does my cron job work in the terminal but not under cron?

Cron runs with a very small PATH, often just /usr/bin:/bin, so it can't find commands the way your login shell can. Use full absolute paths for every command and file, or set a fuller PATH at the top of your crontab.

How can I see what my cron job did?

Add a redirect like >> /path/to/job.log 2>&1 to the end of the crontab line so both normal output and errors are written to a log file. You can also check cron's own activity with grep CRON /var/log/syslog or journalctl -u cron.

Should I use cron or a systemd timer?

For a single simple job, cron is faster to set up and works everywhere. For an important task where you want clear logs in the journal and handling for runs missed while the machine was off, a systemd timer is often the better choice.

TR
Tom Reyes
Support Engineer at Bytte.cloud

Part of the Bytte.cloud team. We run game servers, bots and websites for a living, and we write these guides from what we see day to day in support and on our own servers.

Want to try this on real hardware?

Bytte.cloud has free plans for game servers, bots and websites. No credit card, set up in seconds.

Start for free See the plans