You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/idempotency.mdx
+322-9Lines changed: 322 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,14 +1,44 @@
1
1
---
2
2
title: "Idempotency"
3
-
description: "An API call or operation is “idempotent” if it has the same result when called more than once."
3
+
description: "An API call or operation is idempotent if it has the same result when called more than once."
4
4
---
5
5
6
-
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.
6
+
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. Instead, the original run's handle is returned, allowing you to track the existing run's progress.
7
7
8
+
## Why use idempotency keys?
9
+
10
+
The most common use case is **preventing duplicate child tasks when a parent task retries**. Without idempotency keys, each retry of the parent would trigger a new child task run:
-**Preventing duplicate emails** - Ensure a confirmation email is only sent once, even if the parent task retries
35
+
-**Avoiding double-charging customers** - Prevent duplicate payment processing during retries
36
+
-**One-time setup tasks** - Ensure initialization or migration tasks only run once
37
+
-**Deduplicating webhook processing** - Handle the same webhook event only once, even if it's delivered multiple times
8
38
9
39
## `idempotencyKey` option
10
40
11
-
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:
41
+
You can provide an `idempotencyKey`when triggering a task:
When you pass a raw string, it defaults to `"run"` scope (scoped to the parent run). See [Default behavior](#default-behavior) for details on how scopes work and how to use global scope instead.
107
+
</Note>
108
+
75
109
<Note>Make sure you provide sufficiently unique keys to avoid collisions.</Note>
76
110
77
111
You can pass the `idempotencyKey` when calling `batchTrigger` as well:
The `scope` option determines how your idempotency key is processed. When you provide a key, it gets hashed together with additional context based on the scope. This means the same key string can produce different idempotency behaviors depending on the scope you choose.
127
+
128
+
### Available scopes
129
+
130
+
| Scope | What gets hashed | Description | Use case |
131
+
| --- | --- | --- | --- |
132
+
|`"run"`|`key + parentRunId`| Key is combined with the parent run ID | Prevent duplicates within a single parent run (default) |
133
+
|`"attempt"`|`key + parentRunId + attemptNumber`| Key is combined with the parent run ID and attempt number | Allow child tasks to re-run on each retry of the parent |
134
+
|`"global"`|`key`| Key is used as-is, no context added | Ensure a task only runs once ever, regardless of parent |
135
+
136
+
### `run` scope (default)
137
+
138
+
The `run` scope makes the idempotency key unique to the current parent task run. This is the default behavior for both raw strings and `idempotencyKeys.create()`.
// sendEmail will only be triggered once, even if processOrder retries multiple times
152
+
awaitsendEmail.trigger(
153
+
{ to: payload.email, subject: "Order confirmed" },
154
+
{ idempotencyKey }
155
+
);
156
+
157
+
// ... more processing that might fail and cause a retry
158
+
},
159
+
});
160
+
```
161
+
162
+
With `run` scope, if you trigger `processOrder` twice with different run IDs, both will send emails because the idempotency keys are different (they include different parent run IDs).
163
+
164
+
### `attempt` scope
165
+
166
+
The `attempt` scope makes the idempotency key unique to each attempt of the parent task. Use this when you want child tasks to re-execute on each retry.
// fetchLatestData will run again on each retry, getting fresh data
181
+
const result =awaitfetchLatestData.triggerAndWait(
182
+
{ userId: payload.userId },
183
+
{ idempotencyKey }
184
+
);
185
+
186
+
// Process the fresh data...
187
+
},
188
+
});
189
+
```
190
+
191
+
### `global` scope
192
+
193
+
The `global` scope makes the idempotency key truly global across all runs. Use this when you want to ensure a task only runs once ever (until the TTL expires), regardless of which parent task triggered it.
Even with `global` scope, idempotency keys are still isolated to the specific task and environment. Using the same key to trigger *different* tasks will not deduplicate - both tasks will run. See [Environment and task scoping](#environment-and-task-scoping) for more details.
217
+
</Note>
218
+
219
+
## Default behavior
220
+
221
+
Understanding the default behavior is important to avoid unexpected results:
222
+
223
+
### Passing a raw string
224
+
225
+
When you pass a raw string directly to the `idempotencyKey` option, it is automatically processed with `run` scope:
226
+
227
+
```ts
228
+
// These two are equivalent when called inside a task:
**Breaking change in v4.3.1:** In v4.3.0 and earlier, raw strings defaulted to `global` scope. Starting in v4.3.1, raw strings now default to `run` scope. If you're upgrading and relied on the previous global behavior, you must now explicitly use `idempotencyKeys.create("key", { scope: "global" })`.
235
+
</Warning>
236
+
237
+
This means raw strings are scoped to the parent run by default. If you want global behavior, you must explicitly create the key with `scope: "global"`:
238
+
239
+
```ts
240
+
// For global idempotency, you must use idempotencyKeys.create with scope: "global"
When triggering tasks from your backend code (outside of a task), there is no parent run context. In this case, `run` and `attempt` scopes behave the same as `global` since there's no run ID or attempt number to inject:
When triggering from backend code, the scope doesn't matter since there's no task context. All scopes effectively behave as global.
265
+
</Note>
266
+
90
267
## `idempotencyKeyTTL` option
91
268
92
269
The `idempotencyKeyTTL` option defines a time window during which a task with the same idempotency key will only run once. Here's how it works:
@@ -131,6 +308,32 @@ You can use the following units for the `idempotencyKeyTTL` option:
131
308
-`h` for hours (e.g. `2h`)
132
309
-`d` for days (e.g. `3d`)
133
310
311
+
## Failed runs and idempotency
312
+
313
+
When a run with an idempotency key **fails**, the key is automatically cleared. This means triggering the same task with the same idempotency key will create a new run. However, **successful** and **canceled** runs keep their idempotency key. If you need to re-trigger after a successful or canceled run, you can:
314
+
315
+
1.**Reset the idempotency key** using `idempotencyKeys.reset()`:
2.**Use a shorter TTL** so the key expires automatically:
328
+
329
+
```ts
330
+
// Key expires after 5 minutes
331
+
awaitmyTask.trigger(payload, {
332
+
idempotencyKey: "my-key",
333
+
idempotencyKeyTTL: "5m"
334
+
});
335
+
```
336
+
134
337
## Payload-based idempotency
135
338
136
339
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.
@@ -159,25 +362,135 @@ You can reset an idempotency key to clear it from all associated runs. This is u
159
362
160
363
When you reset an idempotency key, it will be cleared for all runs that match both the task identifier and the idempotency key in the current environment. This allows you to trigger the task again with the same key.
161
364
365
+
### API signature
366
+
367
+
```ts
368
+
idempotencyKeys.reset(
369
+
taskIdentifier: string,
370
+
idempotencyKey: string,
371
+
requestOptions?:ZodFetchOptions
372
+
): Promise<{ id: string }>
373
+
```
374
+
375
+
| Parameter | Description |
376
+
| --- | --- |
377
+
|`taskIdentifier`| The identifier of the task (e.g., `"my-task"`) |
378
+
|`idempotencyKey`| The idempotency key hash to reset (the 64-character hash string) |
379
+
|`requestOptions`| Optional fetch options for the API request |
380
+
381
+
### Resetting keys created with `idempotencyKeys.create()`
382
+
383
+
When you pass an `IdempotencyKey` created with `idempotencyKeys.create()`, the scope and original key are automatically extracted, making it easy to reset:
-`taskIdentifier`: The identifier of the task (e.g., `"my-task"`)
171
-
-`idempotencyKey`: The idempotency key to reset
411
+
### Resetting run-scoped keys
172
412
173
-
After resetting, any subsequent triggers with the same idempotency key will create new task runs instead of returning the existing ones.
413
+
Keys created with `"run"` scope (the default) include the parent run ID in the hash. When resetting from inside the same task, the run ID is automatically available:
If you try to reset a `"run"` or `"attempt"` scoped key from outside a task without providing the required `parentRunId` (and `attemptNumber` for attempt scope), it will throw an error.
460
+
</Warning>
461
+
462
+
### Resetting from the dashboard
463
+
464
+
You can also reset idempotency keys directly from the Trigger.dev dashboard:
465
+
466
+
1. Navigate to the run that has the idempotency key you want to reset
467
+
2. In the run details panel, find the "Idempotency key" section
468
+
3. Click the "Reset" button
469
+
470
+
This is useful when you need to manually allow a task to be re-triggered without writing code.
471
+
472
+

174
473
175
474
<Note>
176
475
Resetting an idempotency key only affects runs in the current environment. The reset is scoped to the specific task identifier and idempotency key combination.
177
476
</Note>
178
477
179
478
## Important notes
180
479
480
+
### Environment and task scoping
481
+
181
482
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.
182
483
183
484
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.
485
+
486
+
### How scopes affect the key
487
+
488
+
The scope determines what gets hashed alongside your key:
489
+
490
+
- Same key + `"run"` scope in different parent runs = different hashes = both tasks run
491
+
- Same key + `"global"` scope in different parent runs = same hash = only first task runs
492
+
- Same key + different scopes = different hashes = both tasks run
493
+
494
+
This is why understanding scopes is crucial: the same string key can produce different idempotency behavior depending on the scope and context.
0 commit comments