-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
docs: clarify idempotency for external side effects #4650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,8 +31,7 @@ sequenceDiagram | |
|
|
||
| Other common use cases include: | ||
|
|
||
| - **Preventing duplicate emails** - Ensure a confirmation email is only sent once, even if the parent task retries | ||
| - **Avoiding double-charging customers** - Prevent duplicate payment processing during retries | ||
| - **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 | ||
| - **One-time setup tasks** - Ensure initialization or migration tasks only run once | ||
| - **Deduplicating webhook processing** - Handle the same webhook event only once, even if it's delivered multiple times | ||
|
|
||
|
|
@@ -63,6 +62,74 @@ export const myTask = task({ | |
|
|
||
| You can use the `idempotencyKeys.create` SDK function to create an idempotency key before passing it to the `options` object. | ||
|
|
||
| <Warning> | ||
| Trigger.dev idempotency keys deduplicate task-trigger requests. They do not make arbitrary side effects inside `run()` exactly-once. | ||
|
|
||
| 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. | ||
| </Warning> | ||
|
|
||
| ## Coordinating payment operations | ||
|
|
||
| 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. | ||
|
|
||
| ```ts trigger/refund.ts | ||
| import { task } from "@trigger.dev/sdk"; | ||
| import Stripe from "stripe"; | ||
|
|
||
| const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); | ||
|
|
||
| export const refundPayment = task({ | ||
| id: "refund-payment", | ||
| run: async (payload: { | ||
| paymentIntentId: string; | ||
| amount: number; | ||
| operationId: string; | ||
| }) => { | ||
| await stripe.refunds.create( | ||
| { | ||
| payment_intent: payload.paymentIntentId, | ||
| amount: payload.amount, | ||
| }, | ||
| { | ||
| // Stripe uses this key to make retries of the same request idempotent. | ||
| idempotencyKey: payload.operationId, | ||
| } | ||
| ); | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ```ts trigger/initiate-refund.ts | ||
| import { task } from "@trigger.dev/sdk"; | ||
| import { refundPayment } from "./refund"; | ||
|
|
||
| export const initiateRefund = task({ | ||
| id: "initiate-refund", | ||
| run: async (payload: { | ||
| refundId: string; | ||
| paymentIntentId: string; | ||
| amount: number; | ||
| }) => { | ||
| // Use an ID that identifies this business operation, not a random value. | ||
| const operationId = `refund:${payload.refundId}`; | ||
|
|
||
| await refundPayment.trigger( | ||
| { | ||
| paymentIntentId: payload.paymentIntentId, | ||
| amount: payload.amount, | ||
| operationId, | ||
| }, | ||
| { | ||
| // Trigger.dev deduplicates this child task trigger when this task retries. | ||
| idempotencyKey: operationId, | ||
| } | ||
| ); | ||
|
Comment on lines
+116
to
+126
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Raw-string idempotency key in the child trigger is run-scoped, which limits the claimed deduplication The example passes Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| }, | ||
| }); | ||
| ``` | ||
|
Comment on lines
+75
to
+129
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Two related file examples are shown as separate code blocks instead of a grouped multi-file example The new two-file refund example is written as two stand-alone code blocks (starting at Documentation convention for multi-file examples
Prompt for agentsWas this helpful? React with 👍 or 👎 to provide feedback. |
||
|
|
||
| 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. | ||
|
|
||
| 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`: | ||
|
|
||
| ```ts | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: triggerdotdev/trigger.dev
Length of output: 27150
🌐 Web query:
Stripe API idempotent requests retention at least 24 hours reused key after pruning official documentation💡 Result:
In the Stripe API, idempotency keys are retained for at least 24 hours [1][2]. After this retention period, Stripe automatically prunes or removes the keys from its system [1][2]. If you reuse an idempotency key after the original request's data has been pruned (i.e., after the 24-hour window), Stripe will treat the subsequent request as a new, unique request rather than a retry of the original [1][3]. When reusing an idempotency key, the Stripe idempotency layer performs a validation check: it compares the parameters of the incoming request against those of the original request [1][4]. If the parameters do not match the original request, Stripe will return an idempotency error to prevent accidental misuse [1][5]. If the parameters do match, the system returns the cached result of the original request, provided the key has not yet been pruned [1][2].
Citations:
Document Stripe’s idempotency retention limit.
Stripe retains API idempotency keys for at least 24 hours and can prune them afterward. If this task replays after pruning, reusing
payload.operationIdcan create a second refund. State this limit. If a refund can replay later, persist the business operation and Stripe refund ID, then reconcile with Stripe before issuing another request.