Skip to content

Commit a80eda6

Browse files
docs: clarify idempotency for external side effects
1 parent 6e77102 commit a80eda6

1 file changed

Lines changed: 69 additions & 2 deletions

File tree

docs/idempotency.mdx

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ sequenceDiagram
3131

3232
Other common use cases include:
3333

34-
- **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
34+
- **Coordinating external side effects** - Use Trigger.dev idempotency to prevent duplicate task triggers, and use the external provider's idempotency mechanism when available to make retries of side-effecting requests safe
3635
- **One-time setup tasks** - Ensure initialization or migration tasks only run once
3736
- **Deduplicating webhook processing** - Handle the same webhook event only once, even if it's delivered multiple times
3837

@@ -63,6 +62,74 @@ export const myTask = task({
6362

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

65+
<Warning>
66+
Trigger.dev idempotency keys deduplicate task-trigger requests. They do not make arbitrary side effects inside `run()` exactly-once.
67+
68+
For external side effects, such as payments, also use the external provider's idempotency mechanism. A task can retry or crash after the provider accepts a request but before the task completes.
69+
</Warning>
70+
71+
## Coordinating payment operations
72+
73+
For a payment operation, use a deterministic business-operation ID to derive idempotency keys for both layers. Trigger.dev deduplicates child task triggers, while your payment provider deduplicates requests for the same operation.
74+
75+
```ts trigger/refund.ts
76+
import { task } from "@trigger.dev/sdk";
77+
import Stripe from "stripe";
78+
79+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
80+
81+
export const refundPayment = task({
82+
id: "refund-payment",
83+
run: async (payload: {
84+
paymentIntentId: string;
85+
amount: number;
86+
operationId: string;
87+
}) => {
88+
await stripe.refunds.create(
89+
{
90+
payment_intent: payload.paymentIntentId,
91+
amount: payload.amount,
92+
},
93+
{
94+
// Stripe uses this key to make retries of the same request idempotent.
95+
idempotencyKey: payload.operationId,
96+
}
97+
);
98+
},
99+
});
100+
```
101+
102+
```ts trigger/initiate-refund.ts
103+
import { task } from "@trigger.dev/sdk";
104+
import { refundPayment } from "./refund";
105+
106+
export const initiateRefund = task({
107+
id: "initiate-refund",
108+
run: async (payload: {
109+
refundId: string;
110+
paymentIntentId: string;
111+
amount: number;
112+
}) => {
113+
// Use an ID that identifies this business operation, not a random value.
114+
const operationId = `refund:${payload.refundId}`;
115+
116+
await refundPayment.trigger(
117+
{
118+
paymentIntentId: payload.paymentIntentId,
119+
amount: payload.amount,
120+
operationId,
121+
},
122+
{
123+
// Trigger.dev deduplicates this child task trigger when this task retries.
124+
idempotencyKey: operationId,
125+
}
126+
);
127+
},
128+
});
129+
```
130+
131+
This does not provide a universal exactly-once guarantee for arbitrary distributed side effects. Trigger.dev deduplicates task triggers, while Stripe uses the idempotency key to safely retry the same refund request.
132+
66133
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`:
67134

68135
```ts

0 commit comments

Comments
 (0)