# Idempotency

> Prevent duplicate events and retries from repeating payments, messages, or provisioning.

Keep a duplicate checkout event from starting another payment run. Use an event or function key to control whether a run starts, and make the payment safe to repeat if its step runs again.

## What is idempotency?

Idempotency, by definition, describes an operation that can occur multiple times without changing the result beyond the initial execution. In the world of software, this means that a functions can be executed multiple times, but it will always have the same effect as being called once. An example of this is an "upsert."

## Decide where to deduplicate

- **At the event producer.** Set a stable event `id` for one business occurrence, such as one checkout. Inngest uses it to avoid starting functions again for a duplicate event received within 24 hours. The duplicate still appears in event history. Reusing an ID for a genuinely new occurrence suppresses work, so choose the boundary carefully.
- **At the function.** Use the function's `idempotency` expression when only one consumer of an event should run once for a business key. The expression reads event data and prevents another execution with the same value for 24 hours. Other functions triggered by that event can still run.
- **At the external service.** Send a stable operation key to a payment, email, database, or provisioning API. A request can succeed before Inngest records the step result. A retry can then call the API again. Event and function deduplication do not remove that risk.

## Choose an idempotency key

Start with the operation whose effect must occur once. Use a checkout ID for a payment, an order and message type for an email, or a customer and resource ID for provisioning. Include every field needed to distinguish valid repeats. Do not use a new retry attempt ID as the external idempotency key.

For example, send the same `id` when retrying delivery of a checkout event:

```typescript
await inngest.send({
  id: `checkout-completed-${checkoutId}`,
  name: "cart/checkout.completed",
  data: { checkoutId },
});
```

If several functions receive that event but only the payment function needs deduplication, set that function's `idempotency` expression from `event.data.checkoutId` instead of deduplicating the whole event. For the exact TypeScript, Go, or Python configuration, use the platform and language reference tab.

## Idempotency at the event level (the producer)

Each event that is received by Inngest will trigger any functions with that matching trigger. If an event is sent twice, Inngest will trigger the function twice. This is the default behavior as Inngest does not know if the event is the same event or a new event.

> **Callout:** Example: Using an e-commerce store as an example, a user can add the same t-shirt to their cart twice because they want to buy two (2 unique events). That same user may check out and pay for all items in their cart but click the "pay" button twice (2 duplicate events).

To prevent an event from being handled twice, you can set a unique event `id` when [sending the event](/docs-markdown/reference/typescript/v4/events/send#inngest-send-event-payload-event-payload-promise). This `id` acts as an idempotency key **over a 24 hour period** and Inngest will check to see if that event has already been received before triggering another function.

```ts
const cartId = 'CGo5Q5ekAxilN92d27asEoDO';
await inngest.send({
  id: `checkout-completed-${cartId}`, // <-- This is the idempotency key
  name: 'cart/checkout.completed',
  data: {
    email: 'taylor@example.com',
    cartId: cartId
  }
})
```

```go {{ title: "Go" }}
cart_id := "CGo5Q5ekAxilN92d27asEoDO"
inngest.Send(context.Background(), inngestgo.Event{
  ID: fmt.Sprintf("checkout-completed-%s", cart_id), // <-- This is the idempotency key
  Name: "cart/checkout.completed",
  Data: map[string]any{"email": "taylor@example.com", "cart_id": cart_id},
})
```

```python {{ title: "Python" }}
cart_id = 'CGo5Q5ekAxilN92d27asEoDO'
await inngest.send({
  id: f'checkout-completed-{cart_id}', // <-- This is the idempotency key
  name: 'cart/checkout.completed',
  data: {
    email: 'taylor@example.com',
    cart_id: cart_id
  }
})
```

| Event ID                                      | Timestamp    | Function                  |
| --------------------------------------------- | ------------ | ------------------------- |
| `checkout-completed-CGo5Q5ekAxilN92d27asEoDO` | 08:00:00.000 | ✅ Functions are triggered |
| `checkout-completed-CGo5Q5ekAxilN92d27asEoDO` | 08:00:00.248 | ❌ Nothing is triggered    |

As you can see in the above example, setting the `id` allows you to prevent duplicate execution on the producer side, where the event originates.

Some other key points to note:

- Event IDs will only be used to prevent duplicate execution for a 24 hour period. After 24 hours, the event will be treated as a new event and will trigger any functions with that trigger.
- Inngest will store the second event and it will be visible in your event history, but it will *not* trigger any functions.
- Events that fan-out to multiple functions will trigger each function as they normally would.

> **Callout:** Tip - If you are using Inngest's webhook transforms, you can set the id in the transform to ensure that the event is idempotent.

> **Callout:** Event idempotency is ignored by some features:DebouncingEvent batchingFunction pausing. While a function is paused, event idempotency is ignored. So if a replay is created after unpausing, it may have "skipped" runs that ignored event idempotency.

## Idempotency at the function level (the consumer)

You might prefer to ensure idempotency at the function level or you may not be able to control the event that is being sent (from a webhook). The [function's `idempotency` config option](/docs-markdown/reference/typescript/v4/functions/create#inngest-create-function-configuration-trigger-handler-inngest-function) allows you to do this.

Each function's `idempotency` key is defined as a [CEL expression](/docs-markdown/durable-execution/guides-and-advanced/writing-expressions) that is evaluated with the event payload's data. The expression is used to generate a unique string key which idempotently prevents duplicate execution of the function.

Each unique expression will only trigger one function execution **per 24 hour period**. After 24 hours, a new event that generates the same unique expression will trigger another function execution.

### Example

We'll use the same example of an e-commerce store to demonstrate how this works. We have an event here with no `id` set ([see above](#at-the-event-level-the-producer)), but we want to ensure that the `send-checkout-email` function is only triggered once for each `cartId` to prevent duplicate emails being sent.

```json {{ title: "Event payload"}}
{
  "name": "cart/checkout.completed",
  "data": {
    "email": "blake@example.com",
    "cartId": "s6CIMNqIaxt503I1gVEICfwp"
  },
  "ts": 1703275661157
}
```

```ts {{ title: "Function definition with idempotency key"}}
export const sendEmail = inngest.createFunction(
  {
    id: 'send-checkout-email',
    // This is the idempotency key
    idempotency: 'event.data.cartId',
    // Evaluates to: "s6CIMNqIaxt503I1gVEICfwp"
    // for the given event payload
    triggers: { event: 'cart/checkout.completed' },
  },
  async  ({ event, step }) => { /* ... */ }
})
```

### Writing CEL expressions

While CEL can do many things, we'll focus on how to use it to generate a unique string key for idempotency. The key things to know are:

- You can access any of the event payload's data using the `event` variable and dot-notation for nested properties.
- You can use the `+` operator to concatenate strings together.

Combining two or more properties together is a good way to ensure the level of uniqueness that you need. Here are couple of examples:

- **User signup:** You only want to send a welcome email once per user, so you'd set `idempotency` to `event.data.userId` in case there your API sends duplicate events.
- **Organization team invite:** A user may be part of multiple organizations in your app. You only want to send a team invite email once per user/organization combination, so you'd set `idempotency` to `event.data.userId + "-" + event.data.organizationId`.

For more information on writing CEL expressions, read [our guide](/docs-markdown/durable-execution/guides-and-advanced/writing-expressions).

> **Callout:** 💡 If you want to control when a function is executed over a period of time you might prefer:rateLimit - Limit the number of function executions per period of timedebounce - Delay function execution for duplicate events over a period of time

### Idempotency keys and fan-out

One reason why you might want to use `idempotency` at the function level is if you have an `event` that fans-out to multiple functions. Let's take the following fan-out example:

| Function       | Event trigger             | How often        |
| -------------- | ------------------------- | ---------------- |
| Track requests | `ai/generation.requested` | Every time       |
| Run generation | `ai/generation.requested` | Once per request |

In this case, you would want to set `idempotency` on the "Run generation" function to ensure that it runs once, for example, for every unique prompt that is sent. You may want to do this as you don't want to re-run the same exact prompt and waste compute resources/credits. However, you still might want to track the number of requests that each user submitted, so you would not want to set `idempotency` on the "Track requests" function. You can see the code for both functions below.

**View the function code**

Both functions use the same event trigger, `ai/generation.requested` which contains a `promptHash` and a `userId` in the event payload.

```ts {{ title: "Track requests function" }}
const trackRequests = inngest.createFunction(
  { id: 'track-requests', triggers: { event: 'ai/generation.requested' } },
  async ({ event, step }) => {
    // Track the request
  }
)
```

```ts {{ title: "Run generation function" }}
const runGeneration = inngest.createFunction(
  {
    id: 'run-generation',
    // Given the event payload sends a hash of the prompt,
    // this will only run once per unique prompt per user
    // every 24 hours:
    idempotency: `event.data.promptHash + "-" + event.data.userId`,
    triggers: { event: 'ai/generation.requested' },
  },
  async ({ event, step }) => {
    // Track the request
  }
)
```

## Check feature interactions

The current event-ID deduplication behavior does not apply to debouncing, event batching, or events received while a function is paused. Design and test those paths separately. Rate limits and debounce control when work starts; they do not replace an idempotency key for an external effect.

## Verify the behavior

Send the same event ID twice in a test environment and inspect the event history and function runs. Then test two valid business occurrences with distinct IDs. Test an external call that succeeds just before the step reports a failure, and confirm the provider accepts the repeated operation key.