Run a cron job at a random time (without sleep $RANDOM hacks)

Cron is wonderful at one thing: doing exactly what you told it, at exactly the minute you told it, forever. That precision is also its limitation. The moment you want a job to fire at a different time on each run — a random time inside a window — cron has nothing built in to help you. There is no "randomize" field in a crontab line. So people reach for workarounds, and most of those workarounds are worse than they look.
This post does two things. First, an honest tour of every common way to fake randomness on top of cron, with code and with the flaws spelled out. Then the one-step way to skip all of it.
Why anyone wants a random cron time in the first place
Three reasons come up again and again.
Spreading load / avoiding the thundering herd. If every server, every customer tenant, and every retry runs 0 3 * * *, they all wake at 03:00:00 together. Upstream APIs see a synchronized spike, caches expire in unison, and one bad minute can take out a whole night of runs. Randomizing the start time smears that load across a window so nothing stampedes. This is the classic SRE motivation, and it is exactly why we wrote up adding jitter to a cron schedule as its own topic.
Making automation look human. A post, a check-in, or a nudge that lands at precisely 09:00:00 every day reads as a bot to everyone downstream — audiences, moderators, and the people you are nudging. Humans do not act on the same second every day. Variable timing is what makes an automated thing feel like it wasn't automated.
Unpredictable sampling. An audit everyone can see coming measures how well people prepare, not how things actually are. Spot checks, QA samples, and drills are only honest when their timing can't be predicted. Random is the whole point, not a nice-to-have.
If none of those apply to you, keep using plain cron. If one of them does, read on.
The workarounds, and what's wrong with each
The sleep $RANDOM prefix
The oldest trick in the book: keep the fixed cron line, but sleep a random amount before the real work.
# crontab: run sometime in the hour after 03:00
0 3 * * * sleep $((RANDOM % 3600)); /usr/local/bin/nightly-job
$RANDOM yields 0–32767 in bash, so % 3600 gives you a 0–59 minute delay. It works, and for a quick load-spread it is genuinely fine. But it has real flaws:
- The worker is occupied the whole time. The process (and whatever slot, container, or connection it holds) sits blocked in
sleepfor up to an hour, doing nothing. Multiply that across many hosts and you are paying for idle waiting. - It hides the real run time. Your logs say the job started at 03:00; the actual work happened at 03:47. Nothing records the true fire time, so debugging "why did this run late" gets harder.
- It isn't uniform across window edges.
RANDOM % 3600is subtly biased (the range doesn't divide evenly), and the moment your window spans an hour boundary — say 02:50 to 03:20 — the single-cron-line-plus-sleep model breaks down, because cron only gave you one anchor minute to offset from. - Drift and overlap. If the job sometimes runs long, a delayed start can collide with the next scheduled run.
sleepgives you no concurrency protection.
cronie's RANDOM_DELAY
On many Linux distros the cron daemon (cronie) supports a RANDOM_DELAY variable at the top of the crontab:
RANDOM_DELAY=45
0 3 * * * /usr/local/bin/nightly-job
This tells cron to add a random delay of up to 45 minutes to every job in that crontab. It's cleaner than a shell sleep because the daemon owns the delay — but it's coarse (whole minutes, applied crontab-wide), it's cronie-specific, and it still offsets from a fixed anchor rather than picking a time inside an arbitrary window.
systemd timers' RandomizedDelaySec
systemd timers do the same idea, better integrated:
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=3600
RandomizedDelaySec=3600 spreads the activation randomly across the following hour. If you already run everything under systemd, this is the tidiest native option. The catch: you have to be on a systemd host, the randomness is a delay-after-anchor rather than a true window, and you're now managing unit files instead of a schedule your teammates or an agent can edit.
OpenBSD cron's ~ field
OpenBSD's cron quietly has the nicest native syntax of the bunch. A ~ in a field means "a random value in the allowed range":
~ 3 * * * /usr/local/bin/nightly-job # random minute, sometime in the 03:00 hour
Genuinely elegant — but it only exists on OpenBSD (and a few ports), so it's not something most teams can rely on.
Jenkins' H (hash) syntax
Jenkins solved the thundering herd for CI with its H symbol. H means "hash this job's name into a stable pseudo-random value in the field's range," so jobs spread out but each stays consistent run to run:
H 3 * * * # a consistent-but-spread minute in the 03:00 hour, per job
This is great for spreading many jobs, but note it is deliberately stable, not fresh-random each run — the opposite of what you want if the goal is unpredictability.
FastCron's R syntax
Among hosted services, FastCron is the one that noticed. It supports an R token in cron expressions and a "Random" option in its UI:
R R * * * # a random minute and hour, chosen per run
Credit where it's due — FastCron is the only classic cron service that treats randomness as a first-class field, and it frames it as a load-spreading utility. It's a real option if you want familiar cron syntax with one randomized field.
GitHub Actions: no native support
If you schedule with GitHub Actions, there is simply nothing. on.schedule.cron takes a standard expression and no randomness modifier, and GitHub itself notes scheduled workflows can be delayed under load but never intentionally jittered. The community workaround is a sleep step inside the job — which brings back every flaw of the shell trick, now billed as Actions minutes.
The pattern behind every workaround
Look at that list and the shape is identical every time: start from a fixed anchor, then bolt on a delay. You keep cron's exact-time model and try to smear it. That's why the seams keep showing — occupied workers, hidden run times, window edges that don't line up, platform-specific syntax you can't reuse.
The alternative is to flip the model: describe the window directly, and let the scheduler pick a fresh time inside it on every run. No anchor, no delay, no sleeping worker.
The one-step way with Untimely
Untimely is a hosted scheduler built around exactly that idea. You give it an interval, a frequency, and a time window; it picks an unpredictable time inside the window each run and calls your webhook (or emails you). No sleep, no delay math, no per-platform syntax — and every fire time is recorded in the event's history, so you can see when it actually ran.
Create the schedule with one authenticated request (make an API key in dashboard settings first):
curl -sS "https://untimely.app/api/events" \
-H "authorization: Bearer $UNTIMELY_API_KEY" \
-H "content-type: application/json" \
-H "idempotency-key: nightly-job-random-001" \
-d '{
"name": "Nightly job at a random time",
"interval": 1,
"frequency": 1,
"betweenTimeStart": "02:00",
"betweenTimeEnd": "04:00",
"actionType": "WEBHOOK",
"webhook": {
"method": "POST",
"url": "https://example.com/jobs/nightly"
}
}'
That event fires once a night, at a different time somewhere inside 02:00–04:00 UTC, and POSTs to your endpoint. Your job stays a plain HTTP handler — no sleeping, no cron daemon to babysit. Want two or three random fires per cycle instead of one? Bump frequency. Want it on weekdays only? Add daysOfWeek. The same model powers a general random-time webhook API if you're triggering systems rather than people.
Because the trigger itself lands at a random time, none of the old problems apply: the worker isn't blocked waiting, the run history shows the true fire time, and the window is a real window — it can span hour boundaries without any special handling.
Which one should you use?
- Just spreading load on one systemd box?
RandomizedDelaySecis fine and native. - On OpenBSD? The
~field is lovely. - Want familiar cron syntax with one random field, hosted? FastCron's
Ris a fair pick. - Want the window to be the schedule — fresh-random each run, run history, email or webhook delivery, and an API an agent can drive — reach for a purpose-built scheduler. That's the gap Untimely fills, and it's why we compare ourselves honestly against classic services like cron-job.org.
Cron is still the right tool when you need the same minute forever. When you need a different believable minute every time, stop bolting sleep onto a fixed anchor and describe the window instead.
About the author
Untimely
Flexible scheduling for recurring prompts, reminders, and small automations that work better inside real-life time windows.
@untimely_app