---
title: Retry policy
description: The built-in exponential-backoff delivery schedule, with optional jitter and injectable randomness.
url: https://pr-1-10d18e06d3bf.thally.app/retry-policy
---

# Retry policy

The built-in exponential-backoff delivery schedule, with optional jitter and injectable randomness.

Sigil ships the delivery schedule as data so that a sender, a dashboard, and
the CLI all quote the same numbers.

```ts
import { nextDelayMs, retrySchedule, totalRetryWindowMs } from '@sigil/core';
```

## The default schedule

`MAX_ATTEMPTS` is **8**. That counts the first delivery, so a failing
endpoint is retried **7** times.

| Retry | Delay before it | Elapsed since first failure |
| ----- | --------------- | --------------------------- |
| 1     | 1 s             | 1 s                         |
| 2     | 2 s             | 3 s                         |
| 3     | 4 s             | 7 s                         |
| 4     | 8 s             | 15 s                        |
| 5     | 16 s            | 31 s                        |
| 6     | 32 s            | 1 min 3 s                   |
| 7     | 64 s            | 2 min 7 s                   |

`retrySchedule()` returns exactly those delays in milliseconds:
`[1000, 2000, 4000, 8000, 16000, 32000, 64000]` — one fewer entry than
`MAX_ATTEMPTS`, because nothing is waited before the first delivery or after
the last failure.

`totalRetryWindowMs()` is **127 000** — two minutes and seven seconds from
first failure to final give-up.

## The curve

`nextDelayMs(attempt)` computes `BASE_DELAY_MS * 2^(attempt - 1)`, clamped
to `MAX_DELAY_MS`. `attempt` is 1-based and counts retries:
`nextDelayMs(1)` is the wait after the first failure.

| Constant       | Value                  |
| -------------- | ---------------------- |
| `BASE_DELAY_MS`| **1 000** (one second) |
| `MAX_DELAY_MS` | **3 600 000** (one hour) |

With the defaults the ceiling is never reached — attempt 7 waits 64 s, well
under an hour. It only matters when `maxAttempts` is raised or the base is
large:

```ts
nextDelayMs(40);                                       // 3600000, clamped
nextDelayMs(3, { baseDelayMs: 100, maxDelayMs: 250 }); // 250
```

The clamp is a `Math.min` over a floating-point power, not a bit shift. A
shift would overflow past attempt 31 and produce a negative delay.

## Jitter

The schedule is deterministic by default. Two senders that fail at the same
instant retry at the same instant — which is exactly what you do not want
when the service they are all retrying against has just come back up.

Set `jitter` to spread them:

```ts
nextDelayMs(4, { jitter: 0.2 });  // somewhere in 6400–8000 ms
retrySchedule({ jitter: 0.2 });   // the whole schedule, spread
```

`jitter` is the fraction of a delay that may be removed, in the range
`(0, 1]`. A value of `0.2` spreads each delay over the 80–100 % band.
Values outside the range throw `invalid_argument`.

Jitter only ever **subtracts**. A retry that waited longer than the published
schedule would push the final attempt past the documented give-up time, and
callers size their dead-letter alerting on that number.

### Keeping it reproducible

Randomness defaults to `Math.random`, which a scheduler that persists "next
attempt at T" cannot recompute after a restart. Pass your own source instead:

```ts
const random = seededRandom(deliveryId);
nextDelayMs(attempt, { jitter: 0.2, random });
```

With a seed derived from the delivery, the same delivery always gets the same
schedule, and two different deliveries still get different ones.

## `BackoffOptions`

| Option       | Type         | Default        | Description                              |
| ------------ | ------------ | -------------- | ---------------------------------------- |
| `baseDelayMs`| `number`     | `1000`         | Delay after the first failure.           |
| `maxDelayMs` | `number`     | `3600000`      | Ceiling on any single delay.             |
| `jitter`     | `number`     | `0` (off)      | Fraction of the delay that may be subtracted, in `(0, 1]`. `0` keeps the schedule exactly reproducible. |
| `random`     | `() => number` | `Math.random` | Source of randomness for `jitter`, returning a value in `[0, 1)`. Injectable so a caller can seed a reproducible generator. |

`retrySchedule` and `totalRetryWindowMs` accept the same options plus
`maxAttempts` (default `MAX_ATTEMPTS`, which is **8**).