# Cron jobs

A cron job calls a path on your production site on a schedule: send the daily digest, clear expired sessions, sync with another API. Open **Project › Cron jobs › New cron job**, give it a name and a path like `/api/cron/digest`, and pick a schedule.

Schedules are standard cron expressions: minute, hour, day of month, month, day of week, read in the job’s time zone (your browser’s, unless you pick another; UTC is in the list too). `0 9 * * *` in Europe/Berlin stays at 09:00 Berlin time through daylight saving changes. A time skipped when clocks spring forward runs right after the jump; a time that happens twice when clocks fall back runs once. The dashboard offers common schedules and shows the next three runs as you type.

| Expression     | Runs                     |
| -------------- | ------------------------ |
| `*/15 * * * *` | Every 15 minutes         |
| `0 * * * *`    | Every hour, on the hour  |
| `0 9 * * *`    | Every day at 09:00       |
| `30 8 * * 1-5` | Weekdays at 08:30        |
| `0 0 1 * *`    | The first of every month |

`@hourly`, `@daily`, `@weekly` and `@monthly` work too.

## Your endpoint

HostEdge sends a `GET` (or `POST` with a small JSON body, if you choose it) to your production URL. A response from 200 to 299 counts as success. Redirects are not followed, so point the job at the final path. Calls give up after 30 seconds.

Every call carries a secret, and every deployment gets it as the `HOSTEDGE_CRON_SECRET` environment variable, so your endpoint can refuse anyone else:

```js
export default {
  async fetch(request, env) {
    const url = new URL(request.url)
    if (url.pathname === '/api/cron/digest') {
      if (request.headers.get('authorization') !== `Bearer ${env.HOSTEDGE_CRON_SECRET}`)
        return new Response('Unauthorized', { status: 401 })
      await sendDigest(env)
      return new Response('ok')
    }
    return env.ASSETS.fetch(request)
  },
}
```

The secret is created with your first cron job; redeploy once so your code sees it. **Roll secret** creates a new one (redeploy again afterwards). The request also has `X-HostEdge-Cron: <job name>` and a `HostEdge-Cron` user agent.

## Retries

Under **If a run fails**, choose up to three retries. A failed scheduled run is tried again 1, 5 and 15 minutes later, as long as that is before the next scheduled run; the request body says `"trigger": "retry"`. A slot counts as failed only when its last retry fails too. Runs you start with **Run now** are not retried.

## History and alerts

Click a job to see its last 50 runs: when, how long, the status code and the start of the response. **Run now** calls it immediately, which is the quickest way to test.

After three failures in a row, members with the Developer role and up get a notification (and an email, unless they turned it off in **Account › Notifications**). The alert fires once until the job succeeds again.

## Limits

| Plan    | Jobs per project | Shortest interval |
| ------- | ---------------- | ----------------- |
| Free    | 2                | 1 hour            |
| Starter | 10               | 5 minutes         |
| Pro     | 50               | 1 minute          |
| Agency  | 100              | 1 minute          |
