# Retry failed requests

Use a bounded retry policy when a request can recover from a temporary network failure, rate limit, or server error. Configure [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) through its `retry` option; you do not need a separate retry loop.

By default, non-2xx responses become [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) instances, and eligible failures pass through the retry policy. A successful operation returns a response; an operation that exhausts its retries rejects with the final error.

The examples run in a browser or Node.js with Fetch support. Install Ky in your project:

```bash
npm install ky
```

The repository README describes the next version of Ky. When using the published package, consult the [current-version documentation](https://www.npmjs.com/package/ky) rather than assuming every README API is available.

## 1. Set eligibility, delays, and timeout budgets

This request fetches users from your API. If it encounters an eligible failure, look for the retry count in the console and the final response status or error. The policy allows three retries after the initial attempt, unless the overall timeout ends the operation first. Replace `https://api.example.com/users` with your API endpoint.

Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to inspect an HTTP failure and [`isTimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#istimeouterror) to identify a timeout. The example reports the status or URL from your result, rather than assuming which failure wins.

```ts
import ky, {isHTTPError, isTimeoutError} from 'ky';

(async () => {
try {
	const response = await ky.get('https://api.example.com/users', {
		timeout: 2000,
		totalTimeout: 10_000,
		retry: {
			limit: 3,
			methods: ['get'],
			statusCodes: [429, 503],
			afterStatusCodes: [429, 503],
			maxRetryAfter: 2000,
			delay: attemptCount => 300 * 2 ** (attemptCount - 1),
			backoffLimit: 1500,
			jitter: true,
			retryOnTimeout: true,
		},
		hooks: {
			beforeRetry: [({retryCount}) => {
				console.log('Starting retry:', retryCount);
			}],
		},
	});
	console.log('Response status:', response.status);
} catch (error) {
	if (isHTTPError(error)) {
		console.log('Final HTTP status:', error.response.status);
	} else if (isTimeoutError(error)) {
		console.log('Timeout for:', error.request.url);
	} else {
		throw error;
	}
}
})().catch(error => console.log('Request ended:', error));
```

`limit` counts retries, not the initial attempt. Set it to a finite, non-negative integer. Numeric shorthand such as `retry: 3` changes only the limit. Automatic retries also require an eligible method; `post` and `patch` are not in the default method list.

`delay` receives the retry attempt count, starting at `1`, and returns milliseconds. The default exponential delay starts at 300 ms, then 600 ms. Full jitter (`jitter: true`) randomizes the computed delay between zero and that value, and `backoffLimit` caps the result. You can instead pass a jitter function that receives the computed delay and returns milliseconds.

For statuses in both `statusCodes` and `afterStatusCodes`, Ky uses `Retry-After` or a supported rate-limit timing header when available. `Retry-After` accepts seconds or an HTTP-date. If it is missing, Ky checks `RateLimit-Reset`, `X-RateLimit-Retry-After`, `X-RateLimit-Reset`, and `X-Rate-Limit-Reset`. Server timing bypasses jitter and `backoffLimit`; use `maxRetryAfter` to cap it. HTTP 413 requires a retry timing header under the default retry checks.

If a retry delay consumes the remaining `totalTimeout` budget, Ky times out without starting another attempt; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for per-attempt, body-read, and overall timeout boundaries.

Enable `retryOnTimeout` to retry timeouts before a response arrives. Timeouts while a body shortcut reads an already received response do not trigger retries.

## 2. Decide whether to retry a failure

Use `shouldRetry` to make the retry decision. It runs only after the limit and method checks pass. Return `true` to override the default checks, `false` to stop, or `undefined` to keep the default logic. Recognized network failures are retried by default for eligible methods; unrecognized errors are not.

This policy allows only two retries for HTTP 429, even though the configured limit is three. Other HTTP 4xx errors stop immediately, and other failures retain the default policy. Replace `https://api.example.com/users` with your API endpoint; a successful request returns its response without retrying.

```ts
import ky, {isHTTPError} from 'ky';

(async () => {
try {
	const response = await ky.get('https://api.example.com/users', {
		retry: {
			limit: 3,
			shouldRetry: ({error, retryCount}) => {
				if (isHTTPError(error)) {
					const status = error.response.status;
					if (status === 429) {
						return retryCount <= 2;
					}
					if (status >= 400 && status < 500) {
						return false;
					}
				}
				return undefined;
			},
		},
	});
	console.log('Response status:', response.status);
} catch (error) {
	console.log('Request ended:', error);
}
})().catch(error => console.log('Request ended:', error));
```

Returning `true` bypasses status checks, `retryOnTimeout`, and server-timing selection; Ky uses the configured delay calculation instead. Return `undefined` when you want the normal status and timing rules to apply.

## 3. Retry based on response content

Return `ky.retry()` from `afterResponse` when a successful HTTP response contains a temporary application error. The hook receives a response clone, so reading its JSON does not consume the response returned to your caller.

For an API that returns `{ "error": { "code": "TEMPORARY_ERROR" } }` with HTTP 200, this example requests another attempt with a one-second delay. Other successful JSON responses pass through to the caller. Replace `https://api.example.com/jobs/42` with your API endpoint.

Use [`isForceRetryError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#isforceretryerror) in `beforeRetry` to identify the forced retry and read its `code`.

```ts
import ky, {isForceRetryError} from 'ky';

const api = ky.extend({
	retry: {limit: 2},
	totalTimeout: 15_000,
	hooks: {
		afterResponse: [async ({response}) => {
			if (response.status === 200) {
				const data = await response.json<{
					error?: {code?: string};
				}>();
				if (data.error?.code === 'TEMPORARY_ERROR') {
					return ky.retry({delay: 1000, code: 'TEMPORARY_ERROR'});
				}
			}
		}],
		beforeRetry: [({error, retryCount}) => {
			if (isForceRetryError(error)) {
				console.log('Forced retry:', retryCount, error.code);
			}
		}],
	},
});

(async () => {
try {
	const result = await api.get('https://api.example.com/jobs/42').json();
	console.log('Final JSON:', result);
} catch (error) {
	console.log('Request ended:', error);
}
})().catch(error => console.log('Request ended:', error));
```

Forced retries still respect `retry.limit` and the overall timeout, but skip the method check and `shouldRetry`. If the response continues to request retries after the limit, the operation rejects with a [`ForceRetryError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#forceretryerror).

Omit `delay` in `ky.retry()` to use the configured retry delay calculation. An explicit `delay` bypasses both jitter and `backoffLimit`. The optional `code` identifies your retry reason in the error passed to hooks.

## 4. Stop a confirmed retry early

`beforeRetry` runs after Ky confirms a retry and waits for its delay. Use it to modify the retry request or throw to stop the operation. If this request encounters repeated eligible failures, it sends a retry-count header on the first retry, then throws before a second retry is sent. Replace `https://api.example.com/users` with your API endpoint.

```ts
import ky from 'ky';

(async () => {
try {
	const response = await ky.get('https://api.example.com/users', {
		retry: {limit: 3},
		hooks: {
			beforeRetry: [({request, retryCount}) => {
				if (retryCount > 1) {
					throw new Error('Stopping after one retry');
				}
				request.headers.set('X-Retry-Count', String(retryCount));
			}],
		},
	});
	console.log('Response status:', response.status);
} catch (error) {
	console.log('Request ended:', error);
}
})().catch(error => console.log('Request ended:', error));
```

Throwing stops the remaining `beforeRetry` hooks and rejects the operation. Prefer throwing to returning `ky.stop`: that symbol resolves the operation with `undefined`, so response property access and body shortcuts such as `.json()` are invalid. If you deliberately use `ky.stop`, await the request without a body shortcut and check that a response exists before accessing it.

Keep retry-attempt changes out of `beforeRequest`, where thrown errors are fatal rather than retried; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the retry decision points and hook stages.

## Options at a glance

The `retry.*` fields belong inside the `retry` object; the timeout options sit alongside it.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `retry.limit` | `number` | `2` | Bounds the number of retries; `0` disables them. |
| `retry.methods` | `readonly string[]` | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | Selects methods eligible for automatic retries. |
| `retry.statusCodes` | `readonly number[]` | `[408, 413, 429, 500, 502, 503, 504]` | Selects HTTP statuses eligible under the default checks. |
| `retry.afterStatusCodes` | `readonly number[]` | `[413, 429, 503]` | Selects eligible statuses that use server retry timing. |
| `retry.maxRetryAfter` | `number` | `Infinity` | Caps server-provided delays in milliseconds. |
| `retry.backoffLimit` | `number` | `Infinity` | Caps calculated delays in milliseconds after jitter. |
| `retry.delay` | `(attemptCount: number) => number` | `attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000` | Calculates delay in milliseconds. |
| `retry.jitter` | `boolean \| ((delay: number) => number)` | `undefined` | Adds randomness to calculated delays. |
| `retry.retryOnTimeout` | `boolean` | `false` | Allows retries for pre-response timeouts. |
| `retry.shouldRetry` | `({error: Error, retryCount: number}) => boolean \| void \| Promise<boolean \| void>` | `undefined` | Overrides or defers to default retry checks. |
| `timeout` | `number \| false` | `10000` | Sets the per-attempt timeout in milliseconds; `false` disables it. |
| `totalTimeout` | `number \| false` | `false` | Bounds the operation in milliseconds; `false` leaves it unbounded overall. |

## Streaming uploads and related tasks

Enabled retries buffer an entire streaming request body in memory so it can be replayed. For large streaming uploads that do not need retries, set `retry: {limit: 0}`. See [Transfer files and streams](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/transfer-files-and-streams).

- [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) explains `throwHttpErrors` and reading HTTP error data.
- [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) explains where each hook runs.
- [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) shows how to share a retry policy across requests.
- [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) covers cancellation with Fetch signals.
