One of the first things you want from your own server is for it to do things while you sleep: run a backup at 3 a.m., restart a flaky service nightly, pull fresh data every fifteen minutes. On Linux, the oldest and most reliable tool for that is cron. It is on practically every VPS already, it needs no daemon you have to install, and once you understand the five-field syntax it stops being intimidating.
The five fields
A cron schedule is five space-separated fields, followed by the command:
┌─ minute (0-59)
│ ┌─ hour (0-23)
│ │ ┌─ day of month (1-31)
│ │ │ ┌─ month (1-12)
│ │ │ │ ┌─ day of week (0-7, 0 and 7 are both Sunday)
│ │ │ │ │
* * * * * command-to-run
A * means "every value." So */15 * * * * is "every 15 minutes," 0 3 * * * is "03:00 every day," and 0 9 * * 1 is "09:00 every Monday." That is 90% of what you will ever write.
Editing your crontab
Each user has their own crontab. Edit yours with:
crontab -e # opens your personal crontab in $EDITOR
crontab -l # list current jobs
Add a line and save. For example, a quarter-hourly sync and a nightly backup:
*/15 * * * * /usr/bin/curl -fsS https://api.example.com/sync >> /var/log/sync.log 2>&1
0 3 * * * /home/deploy/backup.sh
To run something as root (system maintenance, service restarts), use sudo crontab -e, or drop a file into /etc/cron.d/ with an extra username field.
Three things that trip everyone up
1. Cron has almost no PATH. Inside cron, $PATH is minimal, so a bare python or node often "works on the command line but not in cron." Always use absolute paths (/usr/bin/python3), or set PATH= at the top of the crontab.
2. Output goes to mail, or nowhere. If a job prints anything and you do not redirect it, cron tries to email it locally and you never see it. Redirect both streams to a log so failures are visible:
0 3 * * * /home/deploy/backup.sh >> /var/log/backup.log 2>&1
2>&1 is the important half — it captures errors, not just normal output.
3. Timezone. Cron uses the server's system clock. On a multi-region setup your VPS might be on UTC, not your local time. Check with timedatectl, and either do the mental math or set CRON_TZ=Europe/Berlin (or your zone) at the top of the crontab so 0 3 * * * means what you think.
Don't let jobs pile up
If a job can run longer than its interval (a backup that occasionally takes 20 minutes on a 15-minute schedule), guard it with flock so a slow run never overlaps the next one:
*/15 * * * * /usr/bin/flock -n /tmp/sync.lock /home/deploy/sync.sh
-n means "if the lock is held, skip this run rather than queue." Overlapping cron jobs are a classic cause of a server quietly grinding itself to a halt.
When cron is the wrong tool
For sub-minute scheduling, jobs with dependencies, or anything that needs retries and a dashboard, reach for systemd timers or a proper job runner instead. Cron is perfect for simple, independent, time-based tasks — and most of what a small server needs is exactly that.
A quick way to get the syntax right
If you would rather not count asterisks in your head, we keep a free cron expression generator that turns plain-English schedules into the exact five-field line (and explains an existing one back to you). It is one of a handful of small, free tools we host on overnight.host — no signup, just useful. Whether your box lives in an EU or US region, the cron syntax is identical, so the schedule you build there drops straight into crontab -e.
Top comments (0)