Skip to content

Commit b0a57b1

Browse files
feat: Crons (#2922)
Co-authored-by: Jo Franchetti <jofranchetti@gmail.com>
1 parent 40715ab commit b0a57b1

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

deploy/_data.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export const sidebar = [
5454
title: "Domains",
5555
href: "/deploy/reference/domains/",
5656
},
57+
{
58+
title: "Cron",
59+
href: "/deploy/reference/cron/",
60+
},
5761
{
5862
title: "Deno KV",
5963
href: "/deploy/reference/deno_kv/",

deploy/reference/cron.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
---
2+
title: Cron
3+
description: "Scheduling and managing cron jobs in Deno Deploy, including defining cron jobs in code, execution lifecycle, retries, and observability."
4+
---
5+
6+
Cron jobs are scheduled tasks that run automatically on a defined schedule. You
7+
define cron jobs in your code using the `Deno.cron()` API, deploy your
8+
application, and the platform discovers and runs them on schedule.
9+
10+
## Defining cron jobs in code
11+
12+
`Deno.cron()` takes a human-readable name, a schedule, and a handler function.
13+
The name identifies the cron job in the dashboard and logs, the schedule
14+
determines when it fires, and the handler contains the code to run on each
15+
invocation. The schedule can be either a standard
16+
[5-field cron expression](https://en.wikipedia.org/wiki/Cron#UNIX-like) or a
17+
structured object. All times are UTC — this avoids ambiguity around daylight
18+
saving transitions. See the
19+
[full API reference](https://docs.deno.com/api/deno/~/Deno.cron) for details.
20+
21+
```typescript
22+
Deno.cron("cleanup-old-data", "0 * * * *", () => {
23+
console.log("Cleaning up old data...");
24+
});
25+
26+
Deno.cron(
27+
"sync-data",
28+
"*/15 * * * *",
29+
{
30+
backoffSchedule: [1000, 5000, 10000],
31+
},
32+
async () => {
33+
await syncExternalData();
34+
},
35+
);
36+
```
37+
38+
### Common schedule expressions
39+
40+
| Schedule | Expression |
41+
| ------------------------------ | -------------- |
42+
| Every minute | `* * * * *` |
43+
| Every 15 minutes | `*/15 * * * *` |
44+
| Every hour | `0 * * * *` |
45+
| Every 3 hours | `0 */3 * * *` |
46+
| Daily at 1 AM | `0 1 * * *` |
47+
| Every Wednesday at midnight | `0 0 * * WED` |
48+
| First of the month at midnight | `0 0 1 * *` |
49+
50+
### Organizing cron declarations
51+
52+
You must register cron jobs at the module top level, before `Deno.serve()`
53+
starts. The platform extracts `Deno.cron()` definitions at deployment time by
54+
evaluating your module's top-level code — this is how it discovers which cron
55+
jobs exist and what their schedules are. Cron jobs registered inside request
56+
handlers, conditionals, or after the server starts will not be picked up.
57+
58+
As the number of cron jobs grows, keeping them all in your main entrypoint can
59+
get noisy. A common convention is to define cron handlers in a dedicated file
60+
and import it at the top of your entrypoint.
61+
62+
```typescript title="crons.ts"
63+
Deno.cron("cleanup-old-data", "0 * * * *", async () => {
64+
await deleteExpiredRecords();
65+
});
66+
67+
Deno.cron("sync-data", "*/15 * * * *", async () => {
68+
await syncExternalData();
69+
});
70+
```
71+
72+
```typescript title="main.ts"
73+
import "./crons.ts";
74+
75+
Deno.serve(handler);
76+
```
77+
78+
Because `Deno.cron()` calls execute at the module top level, the import alone is
79+
enough to register them — no need to call or re-export anything.
80+
81+
For projects with many cron jobs, you can use a `crons/` directory with one file
82+
per cron job or group of related cron jobs, and a barrel file that re-exports
83+
them:
84+
85+
```typescript title="crons/mod.ts"
86+
import "./cleanup.ts";
87+
import "./sync.ts";
88+
```
89+
90+
```typescript title="main.ts"
91+
import "./crons/mod.ts";
92+
93+
Deno.serve(handler);
94+
```
95+
96+
### Restrictions
97+
98+
There are a couple of restrictions to keep in mind when defining cron jobs. The
99+
name passed to `Deno.cron()` can be at most **256 characters** long — this is
100+
the identifier that appears in the dashboard and logs, so keeping it concise is
101+
good practice regardless. On free organizations, each revision can register at
102+
most **10 cron jobs**. If your project needs more, upgrading to a paid plan
103+
removes this limit.
104+
105+
## Execution lifecycle & status
106+
107+
When a cron job is due, it fires independently on each timeline where it's
108+
registered. Each execution runs the handler from that timeline's active
109+
revision. For example, if a `cleanup-old-data` cron job is registered in both
110+
the production timeline and a `staging` branch timeline, the production
111+
execution runs the handler from the production revision, and the staging
112+
execution runs the handler from the staging revision. Each execution will be
113+
billed as one inbound HTTP request.
114+
115+
Cron job executions progress through these statuses:
116+
117+
| Status | Color | Description |
118+
| ----------- | ------ | --------------------------------------- |
119+
| **Running** | Yellow | Cron job handler is currently executing |
120+
| **OK** | Green | Completed successfully |
121+
| **Error** | Red | Failed — hover to see the error message |
122+
123+
The platform prevents overlapping executions: the same cron job cannot run
124+
concurrently. If a cron job is still running when the next scheduled invocation
125+
is due, that invocation is skipped. This avoids resource contention and ensures
126+
each execution can complete without interference.
127+
128+
## Retries & backoff
129+
130+
By default, failed cron job executions are not retried. You can optionally
131+
provide a `backoffSchedule` array to enable retries and control exactly when
132+
each one happens.
133+
134+
The `backoffSchedule` is an array of delays in milliseconds, where each element
135+
specifies how long to wait before the next retry attempt:
136+
137+
- Maximum **5 retries** per execution
138+
- Maximum delay per retry: **1 hour** (3,600,000 ms)
139+
- Retries do not affect the cron job schedule — the next scheduled run will be
140+
executed on time, even if the previous run has retries pending. If a retry and
141+
the next scheduled run coincide, the later one will be skipped as per the
142+
overlapping policy described above.
143+
144+
Example: `backoffSchedule: [1000, 5000, 10000]` retries up to 3 times with
145+
delays of 1s, 5s, and 10s.
146+
147+
## Dashboard
148+
149+
The **Cron** tab in the app sidebar gives you an overview of all registered cron
150+
jobs across your project. Each entry shows the cron job's schedule, its most
151+
recent executions, and the active [timelines](/deploy/reference/timelines/) it
152+
belongs to. When a cron job with the same name is registered with different
153+
schedules on different timelines, each distinct schedule appears as its own
154+
entry so you can track them independently.
155+
156+
Click "View Details" on any cron job to open its detail page, which shows the
157+
full execution history. You can filter executions using the search bar:
158+
159+
- **`status:<value>`** — filter by status (`ok`, `error`, `running`)
160+
- **`timeline:<name>`** — filter by timeline name (substring match,
161+
case-insensitive)
162+
163+
## Observability integration
164+
165+
Cron job executions produce OpenTelemetry traces. Click "View Trace" in the
166+
execution history to navigate to the
167+
[Observability](/deploy/reference/observability/) traces page for that specific
168+
trace.
169+
170+
On the traces page, you can use these cron-related filters:
171+
172+
- **`kind:cron`** — show only cron spans
173+
- **`cron.name:<name>`** — filter by cron name
174+
- **`cron.schedule:<schedule>`** — filter by schedule expression
175+
176+
## Timelines
177+
178+
Cron jobs run on production and git branch
179+
[timelines](/deploy/reference/timelines/). The platform extracts `Deno.cron()`
180+
definitions at deployment time and schedules them for execution, so each
181+
timeline runs the set of cron jobs defined in its active revision's code. To
182+
add, remove, or modify cron jobs, update your code and deploy a new revision.
183+
Rolling back to a previous deployment re-registers the cron jobs from that
184+
deployment. You can see which cron jobs are currently registered in a given
185+
timeline from its page in the dashboard.

0 commit comments

Comments
 (0)