List of Articles Icon

Knowledge Base

Guides and answers for your VPS, the client area, and billing

Scheduling tasks with cron

What this is

Running commands on a schedule: a nightly backup, a report every hour, a cleanup every five minutes. cron is the traditional Linux scheduler, present on every distribution, and it's the right tool to learn first even though (as this page will argue at the end) it's no longer always the right tool to use. This is the happy path and the discipline; when a job misbehaves, the troubleshooting guide picks up where this leaves off. (On a Windows VPS, the equivalent is Task Scheduler.)

How cron works

A daemon (cron or crond) wakes every minute and runs whatever the loaded crontabs say is due. Each user has their own crontab, and root's is separate from yours:

crontab -e     # edit your crontab
crontab -l     # list it

A job runs with the owning user's permissions, so a job editing system files belongs in root's crontab, and a job touching one user's data belongs in that user's. (System packages also drop schedules into /etc/cron.d/ and the /etc/cron.daily/ family; know they exist so a mystery job can be found, but your own jobs live in crontab -e.)

The crontab format

Each line is five time fields followed by the command:

┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12)
│ │ │ │ ┌─ day of week (0-7, both 0 and 7 are Sunday)
│ │ │ │ │
* * * * *  command

* means "every". A value fixes the field, */n means "every n", and comma lists and ranges work (0,30, 9-17). Worked examples:

30 3 * * *          /usr/local/bin/backup.sh          # every day at 03:30
*/5 * * * *         /usr/local/bin/sync.sh            # every 5 minutes
0 * * * *           /usr/local/bin/report.sh          # on every hour
15 6 * * 1          /usr/local/bin/weekly.sh          # Mondays at 06:15
0 2 1 * *           /usr/local/bin/monthly.sh         # the 1st, at 02:00

Two details that surprise people:

  • Day-of-month and day-of-week combine as OR, not AND. 0 0 13 * 5 runs on the 13th and on every Friday, not only on Friday the 13th. When both fields are restricted, matching either one triggers the job.
  • Shortcuts exist: @daily, @hourly, @weekly, @monthly, and @reboot (run once at boot) can replace the five fields. (@reboot jobs are usually better served by a real systemd unit, which can wait for the network and restart on failure.)

Before trusting any expression, paste it into crontab.guru, it renders the schedule in plain words and catches every one of these surprises.

Writing a job that behaves

The habits that separate a crontab you can trust from one that fails silently:

  • Absolute paths, everywhere. Cron's environment is nearly empty (PATH is typically just /usr/bin:/bin), so write /usr/local/bin/backup.sh, not backup.sh, and have scripts use absolute paths internally too. This single habit prevents the most common cron failure there is.
  • Capture the output. Append >> /var/log/myjob.log 2>&1 so runs and failures leave a record, the 2>&1 matters, errors go to stderr. A cron job whose output goes nowhere fails invisibly. (The fuller menu, MAILTO and dead-man's-switch pings, is in the troubleshooting guide.)
  • Fail loudly inside the script. Start shell scripts with set -euo pipefail so an early error stops the run with a non-zero exit instead of plowing on half-broken.
  • Escape %. In a crontab line, % is special; a date +\%F needs the backslash.
  • Stagger your times. Scheduling everything at 0 0 * * * out of habit means all your jobs stampede at midnight and compete. Pick odd minutes (17 3 * * *), it costs nothing.
  • Right user, least privilege. Not everything needs root's crontab.

The cron cascade: when runs pile up

This is the failure mode that takes whole servers down, and it deserves its own section.

The schedule and the runtime of a job are independent numbers, and cron does not check one against the other: when the next tick arrives, cron starts a new run whether or not the previous one is still going. Consider */5 * * * * on a sync script that takes 2 minutes. Fine, for months. The data grows; the run takes 6 minutes; now a second copy starts before the first finishes. Two copies compete for CPU and disk, so both slow down, so the third copy joins them, and each new arrival makes every running copy slower still. The pile grows without bound, load climbs into the dozens, memory fills, and the VPS stops responding to anything, including, eventually, your SSH session. From the outside it looks like a crashed server; underneath it's one crontab line breeding processes faster than they can finish.

The signature is worth memorizing: "it was fine for months and then the VPS started hanging", because nothing changed except the data quietly growing until runtime crossed the schedule interval.

Spotting it (from the Console if SSH won't answer):

uptime                              # load far above your vCPU count
ps aux | grep -c [s]ync.sh          # how many copies are running right now

A count above 1 for a job that should run alone is the confession. Kill the pile (pkill -f sync.sh), comment the crontab line out, and load falls within minutes; the slow-VPS guide covers the wider cleanup.

Preventing it, three layers, use at least the first:

  1. flock, on any job whose runtime you don't control. It takes a lock before running and, with -n, simply skips the run if the previous one still holds it:

    */5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh

    One overlong run then costs you one skipped tick instead of a meltdown. (Without -n, flock queues the runs instead of skipping, which still piles up processes, for cron protection you almost always want -n.)

  2. timeout, as the backstop for a job that might hang rather than merely run long: timeout 20m /usr/local/bin/sync.sh guarantees no run outlives 20 minutes, so a wedged job can't hold the lock forever.

  3. A schedule with real headroom. Measure the job's actual runtime (time /usr/local/bin/sync.sh) and keep the interval several times larger, and re-check when the data it processes has grown.

When to use a systemd timer instead

Every modern distro ships a second scheduler, and for jobs that matter it's simply better. A systemd timer:

  • cannot cascade: it never starts a run while the previous one is still going, by design, no flock needed, the entire section above becomes impossible;
  • logs every run to journald with full output and exit status (journalctl -u myjob.service), no output-capture plumbing;
  • catches up on missed runs (Persistent=true), where cron silently skips anything that was due while the VPS was off;
  • can cap resources (CPUQuota=, MemoryMax=), so a heavy job can't starve the services it runs beside;
  • has none of cron's environment and % quirks.

The cost is two small unit files instead of one crontab line, and the recipe is in Using systemd timers instead of cron jobs. The rule of thumb from that page holds: if you'd be upset to learn the job hadn't run for a month, it belongs in a timer. Cron remains perfectly fine for the small stuff, a log cleanup, a curl ping, anything where a silent miss costs nothing and the runtime is seconds.

When something doesn't run

The four silent killers (empty environment, %, relative paths, discarded output) and the "did cron even fire?" check live in My cron job didn't run.

Still need help?

You can open a support ticket. So we can help on the first reply, it's worth mentioning:

  • the VPS hostname or IP,
  • the exact crontab line and what the job runs,
  • what uptime and ps aux | grep <your script> show if the problem is load.
  • "How do I schedule a script to run every night / every 5 minutes?"
  • "How does crontab syntax work?"
  • "Why does my cron job with day-of-month and day-of-week run more often than expected?"
  • "My VPS becomes unresponsive and I found many copies of my script running, why?"
  • "How do I stop cron jobs from overlapping?"
  • "What does flock -n do in a crontab?"
  • "Should I use cron or a systemd timer?"
  • "How do I run a command at boot with cron?"
Last reviewed: 2026-07-05