Skip to content

Idempotency

If you trigger a task with the same idempotencyKey twice, the second request returns the original run's handle instead of creating a new run.

Why Use Idempotency Keys?

Most common use case: Preventing duplicate child tasks when a parent task retries.

Other use cases:

  • Preventing duplicate emails
  • Avoiding double-charging customers
  • One-time setup or migration tasks
  • Deduplicating webhook processing

Basic Usage

ts
import { idempotencyKeys, task } from "@trigger.dev/sdk";

export const myTask = task({
  id: "my-task",
  retry: { maxAttempts: 4 },
  run: async (payload: any) => {
    // Scoped to this run — childTask only triggers ONCE across all retries
    const idempotencyKey = await idempotencyKeys.create("my-task-key");
    await childTask.trigger({ foo: "bar" }, { idempotencyKey });
    throw new Error("Retry me"); // retries won't re-trigger child
  },
});

Scopes

ScopeHash inputUse case
"run" (default)key + parentRunIdPrevent duplicates within one parent run
"attempt"key + parentRunId + attemptNumberRe-run child on each parent retry
"global"key onlyEnsure a task runs only once ever

run scope (default)

ts
// Same key in different parent runs = different hashes = both run
const key = await idempotencyKeys.create(`send-confirmation-${payload.orderId}`);

attempt scope

ts
// fetchLatestData re-runs on each parent retry to get fresh data
const key = await idempotencyKeys.create(`fetch-${payload.userId}`, {
  scope: "attempt",
});

global scope

ts
// Welcome email only sent once per user, regardless of how many times triggered
const key = await idempotencyKeys.create(`welcome-email-${payload.userId}`, {
  scope: "global",
});

TTL for Idempotency Keys

ts
// Key expires after 60 seconds — next trigger creates a new run
await childTask.trigger(
  { foo: "bar" },
  { idempotencyKey, idempotencyKeyTTL: "60s" }
);

Supported units: s, m, h, d. Default TTL: 30 days.

Failed Runs

When a run fails, its idempotency key is cleared — the next trigger creates a fresh run. Successful and canceled runs retain their key.

Resetting Keys

ts
import { idempotencyKeys } from "@trigger.dev/sdk";

// Reset a global key
await idempotencyKeys.reset("my-task", "my-key", { scope: "global" });

// Reset a run-scoped key from outside a task
await idempotencyKeys.reset("my-task", "my-key", {
  scope: "run",
  parentRunId: "run_abc123",
});

You can also reset from the dashboard: navigate to a run → find the "Idempotency key" section → click Reset.

Payload-Based Idempotency

ts
import { createHash } from "node:crypto";

function hash(payload: any): string {
  return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
}

const idempotencyKey = await idempotencyKeys.create(hash(childPayload));
await tasks.trigger("child-task", payload, { idempotencyKey });

Breaking Change in v4.3.1

In v4.3.0 and earlier, raw strings defaulted to global scope. From v4.3.1, raw strings default to run scope.

ts
// Before v4.3.1 — was global scope
await childTask.trigger(payload, { idempotencyKey: "my-key" });

// After v4.3.1 — explicitly use global if needed
const key = await idempotencyKeys.create("my-key", { scope: "global" });
await childTask.trigger(payload, { idempotencyKey: key });

Built from official Trigger.dev documentation