---
title: "Schedules"
description: "Standard cron, evaluated in UTC, fired independently of our API."
url: "https://saved.sh/docs/backups/schedules"
---

A backup's schedule is a cron expression. It is validated when you save it, and a backup
that is not `manual` cannot become active without one.

<Callout type="warn">
  **Schedules are evaluated in UTC.** There is no per-backup timezone today, so `0 2 * * *`
  means 02:00 UTC, not 02:00 where you are. Convert before you write it: a team in New York
  wanting 02:00 local writes `0 7 * * *` in winter and `0 6 * * *` in summer, and has to
  choose which of the two matters more.
</Callout>

## Syntax [#syntax]

Standard five-field cron.

```
┌───────── minute        0-59
│ ┌─────── hour          0-23
│ │ ┌───── day of month  1-31
│ │ │ ┌─── month         1-12 or JAN-DEC
│ │ │ │ ┌─ day of week   0-6  or SUN-SAT
│ │ │ │ │
* * * * *
```

| Expression         | Fires                                  |
| ------------------ | -------------------------------------- |
| `0 2 * * *`        | 02:00 every day                        |
| `0 */6 * * *`      | Every six hours, on the hour           |
| `30 3 * * 1-5`     | 03:30 on weekdays                      |
| `0 3 * * 0`        | 03:00 on Sundays                       |
| `0 4 1 * *`        | 04:00 on the first of the month        |
| `0 5 1 1,4,7,10 *` | 05:00 on the first day of each quarter |
| `15 1 * * MON`     | 01:15 on Mondays                       |

Named months and weekdays (`JAN`, `MON`) are accepted and are case-insensitive.

### Shorthand [#shorthand]

| Alias                      | Same as                        |
| -------------------------- | ------------------------------ |
| `@hourly`                  | `0 * * * *`                    |
| `@daily`                   | `0 0 * * *`                    |
| `@weekly`                  | `0 0 * * 0`                    |
| `@monthly`                 | `0 0 1 * *`                    |
| `@yearly`, `@annually`     | `0 0 1 1 *`                    |
| `@15minutes`, `@30minutes` | `*/15 * * * *`, `0,30 * * * *` |

Prefer the explicit form for anything running less than daily. `@daily` fires at midnight
UTC, which is the busiest minute of the day across every workspace we have.

<Callout>
  Spread your schedules. `0 2 * * *` on eleven backups means eleven dumps starting at the same
  second, competing for the same worker, the same disk and the same source. Stagger them:
  `0 2`, `20 2`, `40 2`.
</Callout>

## Choosing a cadence [#choosing-a-cadence]

The question is not "how often can it run" but "how much work am I willing to redo".

| If losing this much work is acceptable | Schedule      |
| -------------------------------------- | ------------- |
| A day                                  | `0 2 * * *`   |
| Six hours                              | `0 */6 * * *` |
| An hour                                | `0 * * * *`   |

Two constraints push the other way:

* **A run must finish before the next one is due.** A dump that takes seven hours cannot be
  on a six-hour schedule.
* **Every run costs.** Each one is billed for computation and for the storage it occupies,
  and each one is an artifact that retention has to sweep. See [Pricing](/docs/billing/pricing).

## Overruns [#overruns]

If a run is still going when the next fire is due, **the scheduled fire is skipped**. Runs do
not stack.

A backup that consistently skips is a backup whose schedule is wrong, and the fix is a longer
interval rather than more concurrency. Two dumps of the same database at once is rarely what
anyone wants.

<Callout type="warn">
  **A manual trigger is the exception: it always runs, even if a run is already in flight.**
  That is deliberate, because "run it now" is usually said by someone who knows what they are
  doing and needs it regardless. Be aware of it when triggering a backup whose scheduled run
  may still be going.
</Callout>

## Triggering outside the schedule [#triggering-outside-the-schedule]

```bash
sctl backup trigger $BACKUP_ID
sctl run list --backup $BACKUP_ID
```

A triggered run is an ordinary run in every other respect: same pipeline, same artifact, same
retention, same billing. It does not shift the schedule, consume the next slot, or reset
anything. The next scheduled fire happens exactly when it would have.

Trigger a backup after any change to it. A schedule you have never watched fire is a
hypothesis.

## Pausing [#pausing]

Pausing suspends the schedule without touching the definition or its artifacts.

```bash
sctl backup pause $BACKUP_ID
sctl backup resume $BACKUP_ID
```

|            | Pause                              | Delete                             |
| ---------- | ---------------------------------- | ---------------------------------- |
| Schedule   | Suspended                          | Gone                               |
| Definition | Kept                               | Gone                               |
| Artifacts  | Kept, and still swept by retention | Blocks the delete while any remain |
| Reversible | Yes                                | No                                 |

Pause during a migration, a maintenance window, or while a source is being rebuilt. Runs are
not queued up while paused: resuming starts from the next scheduled fire rather than
replaying what was missed.

A manual backup has no schedule and therefore cannot be paused.

## Independence [#independence]

Schedules fire from the orchestration layer directly, not from our API. A backup fires on
time even if the API is having a bad day, which is the point of scheduling it there rather
than in a cron loop of ours.

For a `local` backup the fire lands on your worker's queue. If the worker is offline, the run
**queues rather than being skipped**, and executes when the worker returns. See
[lifecycle](/docs/workers/lifecycle#when-the-worker-is-offline).

## Next [#next]

<Cards>
  <Card href="/docs/backups/runs" title="Runs" description="What a fire produces, and how to read it." />

  <Card href="/docs/backups/retention" title="Retention" description="How long the results stay." />
</Cards>
