# Scheduling tasks with cron
Source: https://vpsdime.com/knowledgebase/technical-questions/cron-basics
Last reviewed: 2026-07-05
Summary: How cron works and how to use it well, the crontab format explained field by field with worked examples, the best practices that keep scheduled jobs honest (absolute paths, captured output, staggered times), and the cron cascade: what happens when a new run starts before the old one finishes, why it can drive load high enough to make a VPS unresponsive, and how flock, timeout, or a systemd timer prevents it.

## 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](https://vpsdime.com/knowledgebase/troubleshooting/cron-not-running) picks up where this leaves off. (On a Windows VPS, the equivalent is [Task Scheduler](https://vpsdime.com/knowledgebase/windows-vps/cron-jobs-on-windows).)

## 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](https://vpsdime.com/knowledgebase/troubleshooting/services-not-starting-after-reboot), which can wait for the network and restart on failure.)

Before trusting any expression, paste it into **[crontab.guru](https://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](https://vpsdime.com/knowledgebase/troubleshooting/cron-not-running).)
- **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](https://vpsdime.com/knowledgebase/client-area/services/managing-your-vps) 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](https://vpsdime.com/knowledgebase/troubleshooting/slow-vps) 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](https://vpsdime.com/knowledgebase/technical-questions/systemd-timers). 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](https://vpsdime.com/knowledgebase/troubleshooting/cron-not-running).

## Still need help?

You can [open a support ticket](https://vpsdime.com/open-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.

## Related questions

- "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?"
