---
title: Retries and idempotency
description: Make transient failure safe to retry.
pagination: false
sidebar:
  icon: repeat-2
---

Retry transient failures while using a stable idempotency key to prevent duplicate business work.

> **Before you begin**
>
> Use an existing [task/job with its selected provider installed and
> bound](/docs/modules/jobs/quickstart) before running this example. Core task declarations do not
> install the provider's native SDK.

The default retry policy is three attempts with a one-second initial delay, a 30-second maximum delay, factor two, and no jitter.

```ts title="examples/commerce/src/services/orders/tasks/export-orders.task.ts"
/** Produces demonstration CSV through the ordinary order helper and emits validated progress around the durable export. */
import { defineTask } from "@relkit/app/tasks";
import { z } from "@relkit/app/schema";
import { exportOrderCsv } from "../helpers/fake-business.js";

const exportOrders = defineTask({
  id: "orders.export-orders",
  version: "1",
  execution: "durable",
  input: z.object({ orderIds: z.array(z.string()) }),
  output: z.object({ csv: z.string(), exported: z.number().int().nonnegative() }),
  progress: z.object({
    completed: z.number().int().nonnegative(),
    total: z.number().int().positive(),
  }),
  handler: async ({ orderIds }, context) => {
    await context.progress.emit({ completed: 0, total: orderIds.length });
    const csv = exportOrderCsv(orderIds);
    await context.progress.emit({ completed: orderIds.length, total: orderIds.length });
    return { csv, exported: orderIds.length };
  },
});

export default exportOrders;
```

Set `retry` on `defineTask`, using `maxAttempts`, readable `initialDelay` and `maxDelay`, `factor`, and
`jitter`. A job can add an admission idempotency key and retention policy, but it cannot multiply the
task's retry budget.

Retries are at-least-once. Use an idempotency key for every external side effect and store the result before acknowledging the step. A run ID is not an idempotency key: a retry keeps the run identity but may execute the task again.

The dedupe scope is part of compatibility. Changing it can create duplicate work even when the task source is unchanged, so migration diagnostics treat it as a deliberate change.

Classify permanent validation and authorization failures as terminal. Let transient network, rate-limit, and provider failures retry. Never log credentials or full untrusted input while diagnosing a retry loop.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/jobs` API](/docs/api/jobs)
- [`@relkit/testing` API](/docs/api/testing)

- [`examples/commerce/src/services/orders/tasks/export-orders.task.ts`](https://github.com/rel-kit/relkit/blob/main/examples/commerce/src/services/orders/tasks/export-orders.task.ts)
- [`packages/jobs/tests/task-policy.test.ts`](https://github.com/rel-kit/relkit/blob/main/packages/jobs/tests/task-policy.test.ts)
