AUTOMATION
July 17, 2026

How to Schedule Recurring Tasks the Right Way

6 min read
Author
CloudStick Team
WordPress Engineer
Share this article
How to Schedule Recurring Tasks the Right Way
CloudStick
Scheduling Done Right

Beyond "just add a crontab line"

A scheduled task that merely runs is not the same as a scheduled task you can trust, and the gap between the two is where production incidents live. Adding a line to a crontab takes thirty seconds. But that line says nothing about what happens when the job runs twice at once, fails halfway through, throws its error output into the void, or fires at the exact moment every other job on the server fires. Scheduling done right means designing for those cases up front, because every one of them will eventually happen.

The good news is that the fixes are small and mechanical. Five habits — idempotent jobs, overlap locking, captured output, staggered schedules, and failure alerting — turn a fragile crontab into infrastructure you stop thinking about. Each one is a few minutes of work per job, and together they eliminate the majority of scheduled-task incidents that reach a pager. The rest of this article walks through them in the order they usually matter.

Make every job idempotent

An idempotent job produces the same end state whether it runs once or five times, and that property is the single strongest defense you can build into a schedule. Cron gives you no guarantees about clean execution: a server reboot can interrupt a job mid-write, a duplicate run can start after a manual test, a retry can replay work that already succeeded. If the job is idempotent, none of that matters — you rerun it and the world converges to the correct state.

In practice this means concrete patterns. Write output to a temporary file and move it into place atomically, so a killed job never leaves a half-written backup that looks complete. Make database work check state before changing it — insert-or-update rather than blind insert, so a replay does not duplicate rows. Name generated artifacts deterministically (backup-2026-07-17.sql.gz, not backup-1.sql.gz) so reruns overwrite instead of accumulating. And never design a job that sends an email or charges a card without first recording that it already did — the recorded fact is what makes the retry safe.

Prevent overlapping runs with flock

Cron will happily start a second copy of a job while the first is still running — it fires on schedule, not on completion. A sync that normally takes two minutes will someday take twenty because of a slow API, and if it runs every five minutes you now have four copies fighting over the same files or database rows. The fix ships with every Linux distribution: flock takes an exclusive lock on a file before running your command, and with the -n flag it simply exits if the lock is already held.

# skip this run if the previous one is still going
*/5 * * * * /usr/bin/flock -n /tmp/sync-orders.lock /usr/local/bin/sync-orders.sh
# or wait up to 60 seconds for the lock, then give up
*/5 * * * * /usr/bin/flock -w 60 /tmp/sync-orders.lock /usr/local/bin/sync-orders.sh

Use a distinct lock file per job, and prefer -n for frequent jobs — skipping one five-minute cycle is almost always safer than queueing runs behind a stuck process. This one flag is the cheapest reliability upgrade in this entire article.

Log the output and alert on failure

A scheduled job that discards its output is a job you cannot debug, and the default on most servers is exactly that — cron tries to email output to a local mailbox nobody reads, which on a modern VPS usually means it vanishes. Capture both stdout and stderr to a log file on every job, and you convert next month's mystery into a thirty-second grep.

# capture everything the job prints, errors included
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# ping a dead-man switch only when the job succeeds
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 && curl -fsS https://hc-ping.com/YOUR-UUID > /dev/null

Logging tells you what happened; alerting tells you that something happened at all. The second line above implements a dead-man switch: the job pings a monitoring URL only on success, and the monitor alerts you when the ping stops arriving. This catches the failure mode that exit-code checks miss entirely — the job that never ran, because the server was down, the crontab was deleted, or cron itself stopped.

TIP

Add your job's log file to logrotate so it never fills the disk — a scheduled task whose own log exhausts /var is a classic self-inflicted outage. A five-line config in /etc/logrotate.d/ rotates it weekly and keeps four compressed copies.

Stagger schedules and mind timezones

Midnight is the most congested minute on most servers, because 0 0 * * * is the schedule everyone writes without thinking. Stack a database dump, log rotation, cache rebuild, and three application jobs on the same minute and you get a nightly CPU and I/O spike that slows every one of them — and slows real user traffic too. Spread the load instead: run the backup at 2:17, the cleanup at 3:41, the report at 4:23. Odd minutes cost nothing and buy you a flat load curve; if you graph server stats overnight, the difference is immediately visible.

Timezones deserve the same deliberateness. Cron fires according to the server's system timezone, and a job scheduled for 2 a.m. local time in a daylight-saving zone will run twice one night a year and not at all another. The safest convention is to keep servers on UTC and translate in your head; if a job genuinely must track local business hours, set CRON_TZ at the top of the crontab (supported by the cron shipped with Ubuntu and Debian) rather than changing the system clock. And avoid scheduling anything between 1 a.m. and 3 a.m. local time in a DST zone — that is the window where clocks jump.

Choosing the tool: cron, systemd timers, or Supervisord

The right scheduler depends on the shape of the work, and the decision rule fits in three sentences. Use cron for periodic tasks measured in minutes to months — it is universal, simple, and everything in this article applies to it. Use systemd timers when a job needs what cron lacks natively: journald logging, dependency ordering, resource limits, or Persistent=true so a run missed during downtime executes at the next boot. Use a process supervisor for work that should never stop — queue workers, socket servers, long-running consumers — because a worker restarted by cron every minute is a hack, while a supervisor restarts it the moment it exits.

On a CloudStick-managed server, both ends of that spectrum are in the dashboard: every plan includes a CronJobs manager per website for the periodic work, and Basic plans and above add Supervisord management for always-on workers like Laravel queues or Node processes — so the schedule and the supervisor config live somewhere visible instead of scattered across SSH sessions. Wherever you run it, apply the same audit to every existing job this week: is it idempotent, is it locked, is its output logged, is its schedule staggered, and will anything tell you when it fails? Five questions, and any job that passes all five is one you can safely forget about — which is the entire point of scheduling.

Leave a comment
Full Name
Email Address
Message
Contents