Scheduled tasks

The hourly maintenance tick — what runs on it, what degrades when it stops, and how to confirm it is running.

#Scheduled tasks

Alongside the requests it serves, your instance runs a maintenance tick once an hour, on the 0 * * * * Cloudflare cron trigger. Nothing you do in the app starts it and nothing in the app depends on it directly — which is exactly why it deserves a page of its own. When the tick stops, the API keeps answering normally and the failure is invisible until you go looking for something the tick was supposed to have done.

#What runs on the tick

Eleven tasks run concurrently. Each one is independent: a failure in one is recorded and the rest still run.

Task What it does What degrades if it stops
system-schedulers Re-arms the Durable Object alarm behind every active system schedule flow Scheduled system flows quietly stop firing after any DO state loss
access-log-archive Copies one day of access history to storage for each of the three workspaces furthest behind, oldest gap first, and prunes archived day files for every workspace it examines History passes out of the ~90-day analytics window with nothing durable behind it — there is nothing to re-derive it from
access-log-retention Deletes access.* rows from the audit log past each workspace's own access.retention_days, never past the newest day that workspace has actually archived Nothing is deleted, so the audit table keeps growing — the safe direction, but not the one you configured
stale-runs Marks automation runs stuck in pending/running for over two minutes as failed Orphaned runs accumulate and hold their flow's concurrency slot
blueprint-installs Closes blueprint install runs whose installer died mid-run, marking the rows it left pending as failed with an unknown outcome A crashed install stays in_progress for ever, and the ledger keeps claiming a run is still going
legacy-credentials Tombstones legacy credentials past the retention window Credentials that should have expired remain in the database
backups Takes the scheduled backup when the hour matches your configuration No scheduled backups are taken at all — only manual ones
finalize-migrations Drops the source table of a collection migration whose cooldown has elapsed Superseded tables linger, consuming database storage
usage-rollups Records the last few hours of per-tenant usage into the durable usage ledger Usage history stops accumulating — and because the source retains only ~90 days, the gap is permanent
tenant-backups Takes each workspace's own backup for workspaces that asked, and applies retention Workspaces that switched their own backups on stop getting them — the instance-wide backup is unaffected
messages-outbox Sends messages whose retry has come due, and clears any backlog Every message that failed once stays queued for good — a transient provider blip becomes permanent silence

Most hours, most of these do nothing: the backup task no-ops unless the current UTC hour matches your configured run hour, and the reapers find nothing to reap. That is normal. The tick's value is that it is there on the hour when there is work to do.

Both access-log tasks work per workspace, not just the primary one. Each tick takes a rotating page of up to 50 workspaces; of those, the three furthest behind get a day archived, because building a day file is the only expensive part. If more workspaces are behind than the tick could reach, it says so rather than quietly falling behind. Each workspace's retention window and archive clamp are its own — one workspace's archive progress never licenses deleting another's rows.

Both access-log tasks are off until you switch them on under Settings → Access, and report nothing at all while they are off. access-log-retention is the only task here that deletes data you cannot get back, so it is deliberately separate from the archive it is clamped against: when archiving is on, the prune never deletes past the newest day that has actually reached storage, and it says so on the panel rather than deleting anyway.

#Confirming it is running

System → Monitor → Health shows a Scheduled maintenance panel: when the tick last ran, and the result of every task on that run. Four states matter:

  • Running — the last tick landed on time and every task succeeded.
  • Overdue — no tick has arrived in three hours. The tasks above are not happening. Cloudflare schedules cron triggers on a best-effort basis, so a single late tick is normal; three hours of silence is not.
  • Task stopped — the tick is arriving, but one task is not in it. The panel names the task and when it last ran. This is the state that is easiest to miss and hardest to explain: a task that has stopped still shows the success it was last seen having, so it is not "failing" and the tick is not "overdue".
  • Task failing — the tick is arriving and the task is running, but it fails every time. The panel names the task and shows its error and the last time it did succeed.

The same information is on the public health endpoint, which is the right hook for external monitoring:

curl -s https://your-instance.workers.dev/health | jq .cron
{
	"interval_minutes": 60,
	"stale_after_minutes": 180,
	"last_tick": "2026-07-27T03:00:04.812Z",
	"age_seconds": 254,
	"stale": false,
	"failing": [],
	"stalled": [],
	"tasks": {
		"backups": { "ok": true, "lastSuccess": "2026-07-27T03:00:04.812Z", "lastError": null }
	}
}

Three fields, three different questions, and you need all three:

Field Question What a non-empty value means
stale Is the tick arriving at all? Nothing on the list above is happening.
stalled Is every task still in the tick? Those tasks have not run in over three hours. The others have.
failing Did a task that DID run report an error? Those tasks ran and threw.

stalled and failing are not alternatives — a task can be in both, and a task in stalled alone is the quietest of the three failures. If you are writing one alert rule, alert on all three being empty rather than on stale alone.

Any of the three moves the overall status to degraded — never to unhealthy, because the API is still serving correctly and this endpoint drives uptime alerting.

Note

stale is null, not false, when the instance has never ticked and carries no deploy stamp to measure against. That is the honest answer for a local build, and it deliberately does not raise an alarm.

Note

When stale is true, every task is overdue by definition, so stalled lists all of them. The panel suppresses the per-task flags in that case: the one thing to fix is the tick.

Every tick also writes a system.cron_tick entry to the audit log, so you can answer "did maintenance run overnight?" days after the fact — something the health endpoint, which only holds the most recent tick, cannot tell you.

#If it has stopped

See Scheduled tasks aren't running.

#What you learned

  • An hourly cron trigger runs eleven maintenance tasks; the API keeps working normally when it stops
  • Scheduled backups ride on that tick — no tick, no scheduled backups
  • System → Monitor → Health and GET /health's cron block both report whether it is actually landing
  • stale, stalled and failing answer three different questions — alert on all three, not just the first
  • A stalled tick degrades health rather than failing it, and the audit log keeps the longer history