# Retries

When an attempt fails with a retryable outcome (a 5xx response, a timeout, a network error), cronpad waits and tries again, up to `max_attempts` tries in total. Waits grow exponentially by default and include random jitter so that many failing jobs don't retry in lockstep. A fatal outcome (such as a 404) ends the run immediately.

## Retry policy

```json
"retry": {
  "max_attempts": 3,
  "backoff": "exponential",
  "initial_interval": "10s",
  "max_interval": "10m",
  "multiplier": 2.0,
  "jitter": 0.2
}
```

| Field | Default | Description |
|---|---|---|
| `max_attempts` | `3` | Total tries **including the first**. `1` disables retries. Capped at 10 on cronpad Cloud. |
| `backoff` | `exponential` | `exponential` or `fixed`. |
| `initial_interval` | `10s` | Wait before the first retry (and every retry with `fixed`). |
| `max_interval` | `10m` | Upper bound for any single wait, including `Retry-After` values. |
| `multiplier` | `2.0` | Growth factor for `exponential`. |
| `jitter` | `0.2` | Each wait is randomized by ±20%. `0` disables jitter. |

Server-wide defaults live in [`[retry.defaults]`](../self-hosting/configuration.md#retrydefaults).

## Wait times

Retry *n* waits `min(initial_interval × multiplier^(n−1), max_interval)`, then ±jitter, with the final wait capped at `max_interval`.

With `max_attempts: 6` and the other defaults:

| Attempt | Waits before it (before jitter) | Elapsed since first attempt |
|---|---|---|
| 0 (first) | — | 0 |
| 1 | 10 s | ~10 s |
| 2 | 20 s | ~30 s |
| 3 | 40 s | ~1 m 10 s |
| 4 | 80 s | ~2 m 30 s |
| 5 | 160 s | ~5 m 10 s |

If the endpoint sends `Retry-After` with a 429 or 503, cronpad waits that long instead (capped at `max_interval`).

## What gets retried

Whether an outcome is retryable depends on the sink:

- Webhooks: `retry_on`, by default 5xx, 408, 429, network errors and timeouts. See [Webhooks](../sinks/webhooks.md#responses).
- Commands: `retry_exit_codes` and timeouts. See [Commands](../sinks/commands.md).

## While a run is retrying

- The run's status is `deferred`, with `next_attempt_at` set.
- Retries never delay the job's next scheduled run, unless the job uses `overlap_policy: skip_if_running` ([Misfires and overlap](../schedules/misfires-and-overlap.md)).
- Retries are durable: they're queued in the queue file and tracked in the database, so a restart doesn't lose them.
- Every attempt is listed on the run's page with its outcome and response.

## Retrying by hand

A run that ended `failed` can be retried with **Retry** on the run page or `POST /api/v1/runs/{id}/retry`. This creates a new run with `trigger = retry_manual` for the same scheduled time and a new run ID (and therefore a new idempotency key). Repeated manual-retry requests for that occurrence coalesce into the same retry run; an exhausted manual retry returns `409 conflict`.