A cron job is a task that runs automatically on a time schedule; scheduled Cloud Code applies the idea to serverless functions. The lineage runs from Version 7 Unix through Vixie cron (1987) to today’s platform schedulers, and the model barely changed: a daemon wakes every minute, reads a table of schedule + command lines — the crontab — and fires what matches. What changed is everything around the model: where the clock lives, what happens when a run fails, and whether anyone notices.
Key takeaways
| Question | Answer |
|---|---|
| The syntax | Five fields — minute · hour · day-of-month · month · day-of-week |
| The floor | One-minute granularity; seconds need a different scheduler |
| The gotchas | DST skips and double-fires · the day-of-month/day-of-week OR rule |
| The reliability gap | Classic cron: no retries, no catch-up, no cluster, silent failure |
| The modern form | Schedule as platform config on a function — clock, logs, and retries managed |
Cron in one screen
┌ minute (0–59) ┌ hour (0–23) ┌ day of month (1–31) ┌ month ┌ day of week (0–7)
* * * * * command
0 2 * * * daily at 02:00 */15 * * * * every 15 minutes
0 9 * * 1-5 weekdays at 09:00 0 6,18 * * * 06:00 and 18:00
0 0 1,15 * * the 1st and 15th @daily = 0 0 * * *
@reboot once at daemon start @hourly = 0 * * * *
crontab -e edit your table crontab -l list it
crontab -ri remove (with prompt — bare -r wipes it, no questions asked)
The modern equivalent — the job in code, the schedule in a dashboard, the client just reading results:
// JavaScript — Cloud Code (cloud/main.js)
// A scheduled job: defined in code, scheduled in the dashboard
Parse.Cloud.job('nightlyReport', async (request) => {
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
// Idempotent by date key: a rerun overwrites tonight's report, not duplicates
const existing = await new Parse.Query('DailyReport')
.equalTo('runDate', dateKey(new Date()))
.first({ useMasterKey: true });
const report = existing ?? new Parse.Object('DailyReport');
report.set('runDate', dateKey(new Date()));
report.set('summary', await summarizeOrdersSince(since));
await report.save(null, { useMasterKey: true });
request.message('Report written'); // shows in the dashboard's job status
}); // Flutter / Dart — Back4app Flutter SDK
// Clients consume what the schedule produces — no crontab in sight
final query = QueryBuilder<ParseObject>(ParseObject('DailyReport'))
..orderByDescending('runDate')
..setLimit(1);
final latest = (await query.query()).results?.first as ParseObject?;
print(latest?.get<String>('summary'));
// The report exists because a scheduled Cloud Job ran at 02:00 UTC —
// defined in code, scheduled in the dashboard, logged by the platform. // iOS / Swift — Back4app Swift SDK
// Clients consume what the schedule produces — no crontab in sight
let latest = try await DailyReport.query()
.order([.descending("runDate")])
.first()
print(latest.summary ?? "")
// The report exists because a scheduled Cloud Job ran at 02:00 UTC —
// defined in code, scheduled in the dashboard, logged by the platform. // Android / Kotlin — Back4app Android SDK
// Clients consume what the schedule produces — no crontab in sight
val query = ParseQuery.getQuery<ParseObject>("DailyReport")
query.orderByDescending("runDate")
query.limit = 1
val latest = query.find().firstOrNull()
println(latest?.getString("summary"))
// The report exists because a scheduled Cloud Job ran at 02:00 UTC —
// defined in code, scheduled in the dashboard, logged by the platform. The gotchas the man page knows
Four semantics from crontab(5) that surprise nearly everyone. The OR rule: when both day-of-month and day-of-week are restricted, the job fires when either matches — 0 0 13 * 5 runs on the 13th and every Friday, not only on Friday the 13th. DST, verbatim: jobs scheduled in the spring-forward “missing hour” never run; times that occur twice at fall-back run twice — schedule in UTC, keep critical work out of the local small hours. Steps reset at field boundaries: */90 in the minute field cannot mean “every 90 minutes” — the counter resets each hour; true odd intervals need a real scheduler. The minute floor: the daemon scans once a minute; anything finer is another tool’s job.
The reliability gap
Classic cron is a scheduler, not a reliability system, and the gap has a shape: no retries (a failed run is a failed run); no catch-up (a machine asleep at 02:00 skips the run — anacron and systemd timers’ persistence option exist precisely for this); no cluster story (two servers with the same crontab run everything twice; one server is a single point of failure); silent failure (output mails a local account nobody reads). Modern schedulers answer each gap with named policy: misfire policies (Quartz’s fire-now vs. skip), concurrency policies (Kubernetes’ allow/forbid/replace for overlapping runs), and deadline-based skip semantics — while honestly documenting that distributed scheduling is approximately once: a run may occasionally double or not fire at all. Which yields the discipline both worlds share: scheduled jobs must be idempotent — keyed on their period (the code tabs’ report keyed by date) so a rerun overwrites rather than duplicates, and a missed run is recoverable by running late.
Classic cron vs. systemd timers vs. distributed schedulers vs. scheduled functions
| Classic cron | systemd timers | Distributed (Quartz/K8s-style) | Scheduled cloud functions | |
|---|---|---|---|---|
| Lives | One machine’s crontab | One machine, unit files | A cluster | Platform config on a function |
| Catch-up | None (anacron bolts it on) | Persistent=true | Misfire policies | Platform policy |
| Overlap | Starts another copy | Serialized per unit | allow / forbid / replace | Platform policy |
| Retries | None | Service restart rules | Configurable | Built in |
| Failure visibility | Local mail, unread | journald | Job status objects | Dashboard status + logs |
| Server to keep alive | Yes — and it’s the SPOF | Yes | The cluster | No |
Watching schedules
Scheduled failure is the quietest failure in computing: a job that never fired emits nothing — no exception, no log line, no email — because nothing ran. The pattern that fixes it inverts the alarm: heartbeat monitoring — the job pings a URL on successful completion, the monitor expects the ping within a grace period, and absence triggers the alert. Pair it with the mundane hygiene classic cron never had: run duration tracked over time (the report that took 4 minutes last month and 40 tonight is telling you something), output captured to real logs instead of local mail, and the schedule inventory documented — because a crontab scattered across five servers is how organizations discover jobs they forgot they ran.
Common use cases
- Reports and digests — the nightly rollup the code tabs sketch: aggregate, write, notify.
- Cleanup and expiry — TTL sweeps, orphan pruning, session and token expiration passes.
- Data synchronization — periodic pulls from third-party systems that offer no webhooks.
- Reminders and re-engagement — time-based sends: trial endings, abandoned carts, renewal notices.
- Health and reconciliation — the scheduled audit that catches what event-driven paths missed.
Which scheduler should you use? A decision matrix
| Situation | Reach for |
|---|---|
| One Linux box you already operate | Classic cron — with locks and a heartbeat |
| Laptop-grade or intermittent machines | anacron / systemd timers with persistence |
| A cluster you run anyway | Its native scheduled-job controller |
| App backend on a BaaS | Scheduled Cloud Jobs — no server, logs included |
| Sub-minute or odd intervals | A real scheduler or queue, not crontab arithmetic |
| Event-shaped work mislabeled as scheduled | The job queue — cron only feeds it |
Limitations and trade-offs
- Time triggers describe when, not whether. A schedule fires regardless of whether there’s work; event-driven triggers and queues fit work that arrives irregularly.
- Approximate-once is the honest contract. Even managed schedulers occasionally double-fire or skip; idempotency is the job’s responsibility everywhere.
- Timezones are a policy decision. UTC schedules survive DST; local-time schedules serve humans — pick deliberately, document loudly.
- Frequency has a floor and a price. Minute-level classic cron, platform-dependent floors elsewhere; polling-by-cron at high frequency is usually a queue wearing a clock.
- Schedules accumulate silently. Every “temporary” nightly job is permanent until inventoried; the dashboard listing them all is an underrated feature.
Scheduled Cloud Code on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. Scheduling here is the modern column of the comparison table made concrete: define Parse.Cloud.job in Cloud Code — the code tabs’ nightly report, idempotent by date key, master-key access for the cross-user aggregation — then schedule it in the dashboard’s Background Jobs panel: job name, start time, recurrence, no crontab syntax and no server whose crontab it would live in. Runs, status, and errors land in the server logs and jobs panel — the failure-visibility column answered by default — and the disciplines this article closes with remain yours by design: key jobs to their period, keep them idempotent, and let a heartbeat confirm the platform’s clock and your logic agreed tonight, like every night.
Frequently asked questions
What is a cron job?
A task scheduled to run automatically at fixed times or intervals — classically by the cron daemon on Unix systems reading a crontab, and by extension any time-triggered job on any platform. The name comes from chronos, Greek for time.
What do the five fields in a cron expression mean?
Minute (0–59), hour (0–23), day of month (1–31), month (1–12), day of week (0–7, where both 0 and 7 are Sunday), followed by the command. Asterisk means every value; commas list, dashes range, slashes step.
What are @daily, @hourly, and @reboot?
Shorthand strings replacing the five fields: @hourly, @daily, @weekly, @monthly, @yearly map to their obvious expressions, and @reboot runs once when the daemon starts — a nonstandard but widely supported extension.
How do I create and manage cron jobs?
crontab -e edits your table, crontab -l lists it, and crontab -r removes it entirely — famously adjacent to -e on the keyboard and unprompted, so prefer -ri. Each line is a schedule plus a command; the daemon checks every minute.
What happens if the machine is off at the scheduled time?
Classic cron simply skips the run — there is no catch-up. That is why anacron exists for intermittently powered machines, why systemd timers offer a persistence option, and why modern schedulers make catch-up policy an explicit setting rather than a surprise.
How does cron handle daylight saving time?
Badly, by default: per the manual itself, jobs scheduled in the springtime "missing hour" never run, and times that occur twice at fall-back run twice. The standing advice: schedule in UTC and keep critical jobs out of the local 00:00–03:00 window.
What if a job is still running when the next run starts?
Classic cron happily starts a second copy — and a third. Overlap needs an explicit answer: a lock file that makes the late run skip, or the formalized policies of modern schedulers — allow, forbid, or replace the running instance.
How do I know when a cron job fails?
By default, you don't — output goes to local mail nobody reads, and a schedule that never fires produces no error at all, because nothing ran. The pattern that fixes it: heartbeat monitoring — the job pings a URL on success, and the absence of the ping raises the alarm.
Can cron run a job more often than once a minute?
No — one minute is classic cron's floor; the daemon wakes, scans, and fires per minute. Second-level granularity needs a different scheduler: Quartz-style six-field expressions, a loop inside a service, or an event queue rather than a clock.
Do I need a server to run cron jobs?
Not anymore. Serverless platforms attach a schedule directly to a function: the platform owns the clock, fires the job, records logs and status, and retries per policy — a schedule as configuration rather than a file on a machine you must keep alive.