# Cloudflare Cron Triggers: run a Worker on a schedule

> How to run a Cloudflare Worker on a schedule with Cron Triggers. Nightly jobs, periodic checks, and cleanups, with no server to keep running.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-06-28 | Topics: [Cloudflare](https://flaviocopes.com/tags/cloudflare/) | Canonical: https://flaviocopes.com/cloudflare-cron-triggers/

Not everything happens because someone visits a URL. Some things should just run on their own: a nightly report, a cleanup every hour, a check every five minutes.

For that, Cloudflare has **Cron Triggers**. You give a Worker a schedule, and Cloudflare runs it for you. No server sitting idle, no cron daemon to babysit.

## Add a schedule

Schedules go in `wrangler.jsonc`, under `triggers`:

```jsonc
{
  "triggers": {
    "crons": ["0 2 * * *"]
  }
}
```

That string is a cron expression. `0 2 * * *` means "every day at 2:00." You can have more than one schedule in the array.

A few common ones:

- `*/5 * * * *` — every 5 minutes
- `0 * * * *` — every hour, on the hour
- `0 2 * * *` — every day at 2:00
- `0 0 * * 1` — every Monday at midnight

If cron syntax makes your eyes glaze over, a tool like [crontab.guru](https://crontab.guru/) spells it out in plain English. I also built a free [cron builder](https://flaviocopes.com/tools/cron-builder/) that does the same and previews the next run times.

## Handle the schedule

A normal Worker has a `fetch` handler. A scheduled Worker has a `scheduled` handler instead (or as well):

```js
export default {
  async scheduled(event, env, ctx) {
    console.log('Running the nightly job')
    // do the work here
  },
}
```

Cloudflare calls `scheduled` on your schedule. You get `env` with all your bindings, so you can read your database, write to storage, send email, whatever the job needs.

## A real example

Here's a nightly cleanup that deletes old rows from a D1 database:

```js
export default {
  async scheduled(event, env, ctx) {
    const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000

    await env.DB.prepare(
      'delete from logs where created_at < ?'
    ).bind(thirtyDaysAgo).run()
  },
}
```

Set it to `0 2 * * *` and your logs table cleans itself up every night while you sleep.

## Be careful with time zones

Here's the one thing that trips people up. **Cron schedules run in UTC**, not your local time.

So `0 9 * * *` is 9:00 UTC, which might be the middle of the night where you are. If you want a job at 9:00 your time, convert to UTC first.

## Long jobs and waitUntil

If your job kicks off work that shouldn't block, wrap it in `ctx.waitUntil` so Cloudflare keeps the Worker alive until it finishes:

```js
async scheduled(event, env, ctx) {
  ctx.waitUntil(generateDailyReport(env))
}
```

## Testing it locally

You don't want to wait until 2 AM to know if your job works. In dev, you can trigger it manually:

```bash
npx wrangler dev
```

Then in another terminal, hit the test endpoint Wrangler exposes:

```bash
curl "http://localhost:8787/cdn-cgi/handler/scheduled"
```

That runs your `scheduled` handler right away, so you can see it work.

## What I use it for

Cron Triggers are my go-to for the quiet background chores: rolling up analytics overnight, pruning old data, checking if a site is up, sending a daily digest.

It's the simplest way to run code on a schedule without owning a server. The full reference is in the [Cron Triggers docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/).
