Scheduled execution
Declare `triggers.crons` in runlot.json and your worker's scheduled handler runs on that schedule.
Add triggers to runlot.json and the worker's scheduled handler runs at the times you name.
{
"name": "my-app",
"main": "src/index.ts",
"triggers": { "crons": ["30 18 * * *"] }
}export default {
async fetch(request: Request, env: Env): Promise<Response> {
return new Response("hello");
},
async scheduled(controller: ScheduledController, env: Env, ctx: ExecutionContext) {
// controller.scheduledTime is the time this run was due for, not the current time.
// controller.cron is the expression that fired.
await rebuild(env);
},
};runlot deploy prints the schedules it is about to register, so you can check them before they run.
Scheduled execution (UTC): 30 18 * * *The expression
Five fields, minute hour day-of-month month day-of-week, evaluated in UTC.
| Form | Meaning |
|---|---|
* | every value |
5 | exactly 5 |
1-5 | 1 through 5 |
*/15 | every 15th value |
0,30 | 0 and 30 |
jan, mon | month and weekday names |
Sunday is 0 (and 7 is also accepted). If both day-of-month and day-of-week are restricted, a run happens when either matches — this is how cron has always behaved, so 0 0 1 * 1 means "the 1st of the month, or any Monday."
Expressions are parsed when you deploy. A malformed one, or one that can never fire (0 0 30 2 * — February 30th), is rejected with a 400 naming the field. A schedule that is accepted but never runs would be indistinguishable from a broken deploy, so we do not accept one.
Limits
| Free | Pro | |
|---|---|---|
| Granularity | 1 minute | 1 minute |
| Schedules per organization | 5 | 250 |
| Schedules per project | 5 | 5 |
| Wall-clock per run, schedule under 1 hour | 30 seconds | 30 seconds |
| Wall-clock per run, schedule 1 hour or longer | 5 minutes | 5 minutes |
A run that exceeds its limit is cancelled and recorded as a failure. The budget is smaller for frequent schedules on purpose: what costs us is not holding a schedule, it is waking a project every two minutes.
What to expect
Times are approximate. A run starts at or after the minute you named, not exactly on it. Do not build something that depends on the exact second.
A run that is still going blocks the next one. If a job scheduled every 2 minutes takes 3 minutes, the run that comes due while it is working is skipped and noted in the logs. Without that rule the work would pile up on a project that only has one lane.
Runs that fell far behind are dropped. If the schedule was not served for a while, we run the most recent occurrence once rather than replaying every missed one. An hour of downtime on */2 * * * * would otherwise fire 30 times in a row.
A parked project is woken. Projects stop their process when idle (Stop and resume); a schedule wakes it first and then runs. The wake is part of the run, so an idle project's first scheduled run takes a few hundred milliseconds longer.
A project in cold state runs late. After a long idle period a project's data may be moved off the machine (Cold state and resume). A schedule does not currently pull it back — the run happens after something else brings the project back. If your schedule is the only thing that ever touches the project, keep it hourly or more frequent.
Watching it
Every run leaves a line in the project's logs, whether it succeeded, was skipped, or failed.
runlot logs[runlot] Scheduled run 30 18 * * * finished (1.2s)If your worker does not export scheduled, the run is recorded as a failure and the reason appears in the same place. Deploying triggers without main is rejected — a static-assets deploy has no handler to call.
Changing or removing a schedule
The schedule travels with the code. Edit runlot.json and deploy; rolling back to an earlier version also restores that version's schedules. Remove triggers and deploy to stop them.
What is not here
There is no way to trigger a run by hand, and no history of past runs beyond the log buffer. For work you need to start on demand, expose a route and guard it with a token from runlot secret set.