September 12, 2026
CRON — PUT REPEATING WORK ON A CLOCK

Lesson 26.1 split work by timing: answer the user fast, finish the heavy job later. But some work has no user at all. Nobody clicks "refresh the database at 2 AM." The system must wake itself up. That is what this lesson is for.
Cron is the alarm clock, not the worker
Cron is a schedule that starts work at a chosen time or interval. That is all it is. It does not process files, call models, or write database rows itself — it rings, and something else does the work.
Keep the two roles separate in your head:
- Cron (the alarm): "every day at 6:05 AM Eastern," "every Monday at 8 AM," "every 15 minutes."
- Worker (the one who gets up): the program the schedule triggers, which then fetches, computes, and records.
Why does the distinction matter? Because alarms fail differently than workers. An alarm can ring twice, ring late, or never ring — and your design must survive all three. Lesson 26.4 covers safe repeats fully; this lesson introduces the habit: a good scheduled job can run twice without corrupting data.
Cron covers repeating work your course has already earned: a daily data refresh, a weekly digest email, expiration cleanup (old sessions, stale previews), a scheduled embedding update after new documents arrive, and a synthetic health check that hits your own site every few minutes.
Where the alarm lives: four options
The durable idea — "a schedule triggers a task" — is the same everywhere. The alarm itself can live in several places:
| Option | Good for | Official reference |
|---|---|---|
| Vercel Cron | Scheduled serverless work alongside a Vercel-hosted app | Vercel docs |
| Cloudflare Cron Triggers | Time-based Worker execution at the edge | Cloudflare docs |
| GitHub Actions schedule | Repository automation on a timetable — checks, small maintenance | GitHub docs |
| Cron on a VPS | Full control; a classic cron entry runs any script on your rented computer | Your server's runbook (Class 24) |
Choose by asking where the *work* already lives. If the app is on Vercel, its built-in cron keeps the schedule next to the code. If edge Workers do the fetching, Cloudflare's triggers fit. GitHub Actions schedules are fine for repository chores but are a limited substitute for a production data pipeline — they can be delayed and are not a durable worker. A VPS cron gives maximum control at maximum operating responsibility.
Check the current docs at build time for syntax, limits, and time-zone handling. The concept outlasts every dashboard.
Designing a schedule: three decisions
1. Time zone, stated explicitly. "Daily at 6 AM" is meaningless until you say *whose* 6 AM. Financial data follows market time (US Eastern); a user digest follows the user's time zone. Write it on the schedule: 6:05 AM America/New_York, daily. A schedule without a time zone is a bug that arrives twice a year, at daylight-saving transitions.
2. Frequency that matches how the source changes. Do not refresh hourly when the source publishes monthly. Each wasted run costs compute, burns API quota, and litters logs. Ask: how often does the source actually change, and how fresh must the product be? A filings check can run daily; a macro series refreshes on its release calendar; cleanup can run weekly.
3. A missed run must be observable. Schedules get skipped — deploys, provider hiccups, daylight-saving edges. If nobody notices, you get Lesson 26.5's nightmare: three days of stale data behind a green homepage. Every cron job needs a success signal written somewhere durable (a job_runs row, a log line) and something that complains when the signal is absent.
Worked example: FRED refresh done right
The product needs the unemployment rate (FRED series UNRATE, monthly, usually released the first Friday of the month). A naive design — "fetch UNRATE every hour" — wastes runs and risks writing duplicates.
The deliberate design:
- Schedule: daily at 7:00 AM America/New_York — *after* the expected release window, not before. There is no prize for fetching at midnight when the release lands Friday morning.
- Record the source timestamp: store the observation with its vintage — series ID, observation date, value, units,
released_at,retrieved_at. Keep the prior value; never overwrite history with the latest fetch. - Flag missing, never invent: if the release is late (holiday, delayed publication), write a
job_runsrow markedsource-missing, keep serving the prior value labeled "as of May 2026," and surface a banner. The product says "FRED UNRATE not yet released — showing prior month" instead of silently copying last month's number into this month's row. - Double-run safety (preview of 26.4): the job's identity is
fred/UNRATE/2026-06. If the schedule fires twice, the second run finds the row for that identity and updates it rather than appending a twin. Same input, same result — no duplicates.
cron (daily 7:00 AM ET)
→ worker fetches FRED UNRATE
→ writes observation keyed by (series_id, obs_date)
→ writes job_runs row (started, finished, rows_written, source_status)
→ freshness dashboard reads latest job_runs row
Exercise: write a cron card
Make one cron card for a real repeating task in your project. Copy this template exactly:
## Cron card: <task name>
- Task: <what the worker does, one sentence>
- Source: <provider + docs URL, e.g. FRED API>
- Schedule + time zone: <e.g. daily 7:00 AM America/New_York>
- Expected duration: <e.g. under 2 minutes>
- Output record: <table + key, e.g. observations keyed by (series_id, obs_date)>
- Success signal: <e.g. job_runs row status=ok + fresh retrieved_at>
- If source is late: <e.g. keep prior value, mark stale, show "as of" date>
Finish line: one completed cron card with no blank fields.
Verification: check yesterday's (imaginary or real) run against the card — can you point to the output record and the success signal for that date? If either is missing, the card describes a hope, not a schedule.
Common failure mode: a card whose "late plan" is "retry until it works." Unbounded silent retries against a missing source burn quota and hide the real signal. Flag, label stale, and alert a human instead.
Check your understanding
- Why is "cron is the alarm, not the worker" more than wordplay? Name one failure each side can have.
- Your source publishes monthly. What breaks if you schedule an hourly refresh?
- A scheduled job ran twice. What single design choice decides whether you get one correct row or two duplicate rows?
Next
Alarms start work on time. But what happens when 500 users arrive at once and every alarm rings together? Lesson 26.3 turns that traffic jam into an orderly line — queues and the workers that drain them.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
