We currently support idempotency at the task level, meaning that if you trigger a task with the same idempotencyKey twice, the second request will not create a new task run.

In version 3.3.0 and later, the idempotencyKey option is not available when using triggerAndWait or batchTriggerAndWait, due to a bug that would sometimes cause the parent task to become stuck. We are working on a fix for this issue.

idempotencyKey option

You can provide an idempotencyKey to ensure that a task is only triggered once with the same key. This is useful if you are triggering a task within another task that might be retried:

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

export const myTask = task({
  id: "my-task",
  retry: {
    maxAttempts: 4,
  },
  run: async (payload: any) => {
    // This idempotency key will be unique to this task run, meaning the childTask will only be triggered once across all retries
    const idempotencyKey = await idempotencyKeys.create("my-task-key");

    // childTask will only be triggered once with the same idempotency key
    await childTask.trigger({ foo: "bar" }, { idempotencyKey });

    // Do something else, that may throw an error and cause the task to be retried
    throw new Error("Something went wrong");
  },
});

You can use the idempotencyKeys.create SDK function to create an idempotency key before passing it to the options object.

We automatically inject the run ID when generating the idempotency key when running inside a task by default. You can turn it off by passing the scope option to idempotencyKeys.create:

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

export const myTask = task({
  id: "my-task",
  retry: {
    maxAttempts: 4,
  },
  run: async (payload: any) => {
    // This idempotency key will be globally unique, meaning only a single task run will be triggered with this key
    const idempotencyKey = await idempotencyKeys.create("my-task-key", { scope: "global" });

    // childTask will only be triggered once with the same idempotency key
    await childTask.trigger({ foo: "bar" }, { idempotencyKey });
  },
});

If you are triggering a task from your backend code, you can use the idempotencyKeys.create SDK function to create an idempotency key.

import { idempotencyKeys, tasks } from "@trigger.dev/sdk/v3";

// You can also pass an array of strings to create a idempotency key
const idempotencyKey = await idempotencyKeys.create([myUser.id, "my-task"]);
await tasks.trigger("my-task", { some: "data" }, { idempotencyKey });

You can also pass a string to the idempotencyKey option, without first creating it with idempotencyKeys.create.

import { myTask } from "./trigger/myTasks";

// You can also pass an array of strings to create a idempotency key
await myTask.trigger({ some: "data" }, { idempotencyKey: myUser.id });
Make sure you provide sufficiently unique keys to avoid collisions.

You can pass the idempotencyKey when calling batchTrigger as well:

import { tasks } from "@trigger.dev/sdk/v3";

await tasks.batchTrigger("my-task", [
  {
    payload: { some: "data" },
    options: { idempotencyKey: await idempotencyKeys.create(myUser.id) },
  },
]);

idempotencyKeyTTL option

By default idempotency keys are stored for 30 days. You can change this by passing the idempotencyKeyTTL option when triggering a task:

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

export const myTask = task({
  id: "my-task",
  retry: {
    maxAttempts: 4,
  },
  run: async (payload: any) => {
    const idempotencyKey = await idempotencyKeys.create("my-task-key");

    // The idempotency key will expire after 60 seconds
    await childTask.trigger({ foo: "bar" }, { idempotencyKey, idempotencyKeyTTL: "60s" });

    await wait.for({ seconds: 61 });

    // The idempotency key will have expired, so the childTask will be triggered again
    await childTask.trigger({ foo: "bar" }, { idempotencyKey });

    // Do something else, that may throw an error and cause the task to be retried
    throw new Error("Something went wrong");
  },
});

You can use the following units for the idempotencyKeyTTL option:

  • s for seconds (e.g. 60s)
  • m for minutes (e.g. 5m)
  • h for hours (e.g. 2h)
  • d for days (e.g. 3d)

Payload-based idempotency

We don’t currently support payload-based idempotency, but you can implement it yourself by hashing the payload and using the hash as the idempotency key.

import { idempotencyKeys, task } from "@trigger.dev/sdk/v3";
import { createHash } from "node:crypto";

// Somewhere in your code
const idempotencyKey = await idempotencyKeys.create(hash(childPayload));
// childTask will only be triggered once with the same idempotency key
await tasks.trigger("child-task", { some: "payload" }, { idempotencyKey });

// Create a hash of the payload using Node.js crypto
// Ideally, you'd do a stable serialization of the payload before hashing, to ensure the same payload always results in the same hash
function hash(payload: any): string {
  const hash = createHash("sha256");
  hash.update(JSON.stringify(payload));
  return hash.digest("hex");
}

Important notes

Idempotency keys, even the ones scoped globally, are actually scoped to the task and the environment. This means that you cannot collide with keys from other environments (e.g. dev will never collide with prod), or to other projects and orgs.

If you use the same idempotency key for triggering different tasks, the tasks will not be idempotent, and both tasks will be triggered. There’s currently no way to make multiple tasks idempotent with the same key.