Cron split — Cloudflare Cron Triggers + GitHub Actions schedule, by job shape
Cron split — Cloudflare Cron Triggers + GitHub Actions schedule, by job shape
Decision
The family runs cron jobs on both Cloudflare Cron Triggers and
GitHub Actions schedule:. The substrate is picked by the job's
shape, not by convenience:
- Cloudflare Cron Triggers ? jobs that live inside a Worker. Need Worker bindings (KV, R2, D1, Service Bindings), need sub-minute latency, run on a 1–60 min cadence, touch only first-party state.
- GitHub Actions
schedule:? jobs that need a build environment. Needpnpm/wrangler/gh, need repo checkout, need to commit results back, need to publish to npm / a registry / a deploy target.
Why
- They aren't substitutes — they're different substrates. CF Cron has no
pnpm; GH Actions has 5–15 min jitter and runner cold starts that disqualify it for sub-minute jobs. - Both are free at our scale on the Spark / Free / public-repo tier — no card, no quota cliff for the volumes the family actually runs.
- Splitting by job shape makes the answer to "where does this cron live" mechanical: read the job description, pick the substrate. No ambiguity, no per-team taste.
- Concretely: oriz-omnipost's RSS cross-post commits
state.jsonback to the repo — that's a GH Actions job. The Worker's idempotency-table sweep only touches Worker KV — that's a CF Cron Triggers job. Same family, two substrates, zero overlap.
Implications
- New
services/business/cron/subdir holdscloudflare-cron-triggers.mdandgithub-actions-schedule.md. The shared GitHub Actions service entry atservices/infra/compute/github-actions.mdkeeps documenting CI; the cron-shaped facet lives inservices/business/cron/. - Every cron job in the family declares which substrate it lives on in its file header (workflow YAML or
wrangler.toml). - Latency-sensitive jobs (RSS poll, idempotency sweeps, cache rebuilds) move to CF Cron when introduced. Build / publish / commit-back jobs stay on GH Actions.
- Heartbeat to healthchecks.io is fired from both substrates — one heartbeat URL per substrate, so a substrate outage shows up as a missed heartbeat for that substrate alone.
- Worker quota math (100K req/day) treats CF Cron invocations as Worker requests; this is fine — every cadence we run sums to <1K invocations/day per Worker, well clear of the cap. Documented in
never-hit-quotas.mdheadroom math. - No third cron substrate. Adding one (Vercel Cron, Deno Deploy cron, self-hosted) is rejected — it doesn't unlock a job shape these two can't cover, and it adds another quota to never hit.
Decision matrix (the on-ramp question for new jobs)
Does the job need pnpm / wrangler / gh / repo checkout / npm publish?
+-- YES ? GitHub Actions schedule
+-- NO ? Does it need < 5 min latency or Worker bindings?
+-- YES ? Cloudflare Cron Triggers
+-- NO ? Either; default to CF Cron Triggers (cheaper minutes-budget)