Cron is the quiet workhorse of computing. Right now, on servers all over the world, jobs are firing without a human anywhere near a keyboard — backups running at 2 a.m., logs rotating at midnight, reports emailed every Monday, caches warmed every five minutes. Nobody remembers to do these things. Nobody has to. That is the whole point of scheduled tasks: you describe what should happen and when, once, and then you forget about it forever. This is the guide to doing that well — on Linux, on Windows, and inside WaSQL.
Cron traces back to Version 7 Unix in the late 1970s at AT&T Bell Labs. The name comes from Chronos, the Greek word for time. The version most Linux systems run today — Vixie cron — was written by Paul Vixie in 1987 and has barely needed to change since, which tells you how well the original design held up.
The idea is simple: a daemon (crond) wakes up once a minute, reads a set of tables describing scheduled jobs, and runs anything whose time has come. Those tables are called crontabs (cron tables). Decades later, that exact model still powers most of the automation on the internet.
A crontab entry is five time fields followed by the command to run. Master these five fields and you understand cron completely:
* * * * * command_to_run
│ │ │ │ │
│ │ │ │ └─ day of week (0-7, where 0 and 7 both mean Sunday)
│ │ │ └─── month (1-12)
│ │ └───── day of month (1-31)
│ └─────── hour (0-23)
└───────── minute (0-59)
Each field accepts four operators:
| Operator | Meaning | Example | Reads as |
|---|---|---|---|
* |
every value | * * * * * |
every minute |
, |
a list | 0 8,12,17 * * * |
at 8am, noon, and 5pm |
- |
a range | 0 9 * * 1-5 |
9am Monday through Friday |
/ |
a step | */15 * * * * |
every 15 minutes |
A few real-world entries to make it concrete:
# Every day at 2:00 AM — nightly backup
0 2 * * * /home/steve/scripts/backup.sh
# Every 5 minutes — poll a queue
*/5 * * * * /usr/bin/php /var/www/worker.php
# 9:00 AM on weekdays only — send the morning report
0 9 * * 1-5 /home/steve/scripts/report.sh
# Midnight on the first of every month — reset counters
0 0 1 * * /home/steve/scripts/monthly_reset.sh
# 3:30 AM every Sunday — heavy weekly maintenance
30 3 * * 0 /home/steve/scripts/vacuum.sh
Cron also understands a handful of named schedules that replace the five fields entirely — easier to read and harder to get wrong:
@reboot run once, when the machine boots
@hourly 0 * * * *
@daily 0 0 * * * (also @midnight)
@weekly 0 0 * * 0
@monthly 0 0 1 * *
@yearly 0 0 1 1 * (also @annually)
So @daily /home/steve/scripts/backup.sh is the same as 0 0 * * * /home/steve/scripts/backup.sh, just friendlier.
Tip: If you ever stare at five fields and can't decode them, paste the line into crontab.guru. It translates cron syntax into plain English and is the fastest way to sanity-check a schedule.
You never edit the crontab file directly. You use the crontab command, which validates your changes and installs them for the daemon:
crontab -e # edit your crontab (opens $EDITOR)
crontab -l # list your current jobs
crontab -r # remove ALL your jobs — careful, no confirmation
crontab -u www -e # edit another user's crontab (as root)
Because crontab -r wipes everything with no prompt and sits one key away from -e on the keyboard, get in the habit of backing up first:
crontab -l > ~/crontab.backup
There are two kinds of crontab, and the difference trips people up:
crontab -e) run as you and have no user column — the five time fields go straight to the command./etc/crontab) and drop-in files in /etc/cron.d/ add a sixth field: the user to run as.# /etc/crontab — note the extra 'user' field before the command
0 3 * * * root /usr/local/bin/system-maintenance.sh
For packaged software, the tidiest approach is a single file in /etc/cron.d/. And for the common cases, the system already ships directories named /etc/cron.hourly/, /etc/cron.daily/, /etc/cron.weekly/, and /etc/cron.monthly/ — drop an executable script into one of those and it just runs on that cadence. No syntax required.
Three things account for the vast majority of "my cron job didn't run" tickets:
PATH and none of your shell's aliases or profile. Always use absolute paths — /usr/bin/php, not php. Set any variables you need at the top of the crontab:
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=steve@example.comMAILTO (if set) or lost. Capture it yourself so you have a trail:
0 2 * * * /home/steve/backup.sh >> /var/log/backup.log 2>&1flock so a new run skips if the last one is still going:
*/5 * * * * /usr/bin/flock -n /tmp/worker.lock /var/www/worker.shatCron is for repeating work. When you need something to run once at a future time, reach for at:
echo "/home/steve/deploy.sh" | at now + 30 minutes
echo "/home/steve/reboot.sh" | at 04:00 tomorrow
atq # list pending one-off jobs
atrm 3 # cancel job number 3
On most current Linux distributions, systemd offers timers as a more powerful replacement for cron. A timer is two small files: a .service describing what to run and a .timer describing when.
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/home/steve/scripts/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable and start it, then inspect all timers on the system:
sudo systemctl enable --now backup.timer
systemctl list-timers # see every timer and its next run
journalctl -u backup.service # full logs, automatically captured
Why bother, when cron already works? Timers give you three things cron can't:
journalctl — no more redirecting to log files by hand.Persistent=true — if the machine was asleep or off when a job was due, the timer runs it as soon as it comes back. Cron simply misses it.Cron is still perfect for simple, portable jobs. Reach for timers when you need reliability guarantees and good observability.
macOS note: Apple deprecated cron in favor of
launchd. Jobs are.plistfiles in~/Library/LaunchAgents, loaded withlaunchctl load. Classic cron still functions on a Mac, but launchd is the blessed path.
Windows has no cron. Its equivalent is the Task Scheduler, and everything you can do in its GUI (taskschd.msc) you can also do from the command line — which is what you want for automation and repeatable setup.
schtasks command:: Create a task that runs every day at 2:00 AM
schtasks /create /tn "NightlyBackup" /tr "C:\scripts\backup.bat" /sc daily /st 02:00
:: Every 5 minutes
schtasks /create /tn "PollQueue" /tr "C:\scripts\worker.bat" /sc minute /mo 5
:: Inspect, run on demand, and remove
schtasks /query /tn "NightlyBackup" /v /fo LIST
schtasks /run /tn "NightlyBackup"
schtasks /delete /tn "NightlyBackup" /f
Modern Windows favors PowerShell's ScheduledTasks cmdlets, which are more expressive and scriptable:
$action = New-ScheduledTaskAction -Execute "C:\scripts\backup.bat"
$trigger = New-ScheduledTaskTrigger -Daily -At 2:00AM
Register-ScheduledTask -TaskName "NightlyBackup" `
-Action $action -Trigger $trigger `
-Description "Nightly backup job"
Get-ScheduledTask -TaskName "NightlyBackup"
Unregister-ScheduledTask -TaskName "NightlyBackup" -Confirm:$false
The concepts map cleanly onto cron: an action is the command, a trigger is the schedule. The main practical difference is that Windows tasks carry their own security context — pay attention to which user the task runs as and whether it should run when that user is logged off.
Here's where it gets interesting. WaSQL doesn't ask you to touch a crontab or Task Scheduler for each job. Instead it stores every scheduled job as a record in the _cron database table — which means your jobs travel with your database, sync between environments, and are managed entirely from the admin UI. No shell access required to add, edit, pause, or inspect a job.
One OS-level entry drives everything. You register a single scheduled task with the operating system that runs WaSQL's cron.php (or cron_worker.php) every minute:
# Linux crontab — the ONLY OS cron entry WaSQL needs
* * * * * /var/www/wasql_live/php/cron.sh >/var/www/wasql_live/php/cron.log 2>&1
On Windows, you create one Scheduled Task that runs the same script every minute. cron.php is command-line only (it refuses to run from a URL for security). Each minute it wakes, connects to every configured database, and processes that database's _cron queue.
Each _cron record describes one job. The important fields:
run_cmd — what to run. WaSQL auto-detects one of four types:
https://…) — fetched via an internal request,/reports/daily/regionA,<?=someFunction();?>),frequency — an interval in minutes (e.g. 5 = every five minutes),run_format + run_values — match a PHP date() format against a list of values,run_format with month / day / hour / minute arrays (use -1 for "any"), which is the full cron-style calendar.active, paused, begin_date, end_date — turn jobs on/off and bound them to a date window.run_as — run the job as a specific WaSQL user, using a temporary auth code, so page-based crons see the right permissions.records_to_keep — how much run history to retain.It's concurrency-safe. WaSQL uses a cron_pid handshake: when a worker picks up a job it stamps its process ID and re-reads the record to confirm it won the race. That means you can run multiple workers in parallel for heavy load, and no job ever runs twice at once.
Everything is logged. Each run records its output, duration, and memory into the _cronlog table (run_result, run_length, run_memory), automatically trimmed to records_to_keep. You get a complete, searchable execution history in the admin interface — no digging through /var/log. And if you need a job to fire immediately, a "Run Now" action drops a small trigger file that the next cycle picks up instantly.
The net effect: the operating system's scheduler does exactly one boring thing — call WaSQL once a minute — and all the real scheduling logic lives in your database where it's portable, auditable, and manageable from a browser.
Scheduled tasks are one of those skills that pay for themselves the first week you learn them. The five-field cron syntax is worth committing to memory; crontab.guru covers the rest. On modern Linux, graduate to systemd timers when you need reliability and logging. On Windows, schtasks and the PowerShell cmdlets give you the same power as the GUI in a form you can script. And if you're building on WaSQL, you get all of it — scheduling, logging, multi-database support, and a proper UI — backed by a single _cron table and one humble OS entry ticking away once a minute.
Set it, and forget it.