withIdempotency
Middlewares

withIdempotency

Deduplicate sends within a time window

withIdempotency caches the result of a send and replays it for matching keys instead of re-sending. On the first send for a given key, the pipeline runs normally and the result is stored. On subsequent sends with the same key within the TTL, the cached result is returned and the downstream pipeline never runs.

Failures are not cached — a thrown error leaves the slot empty so the next attempt is treated as fresh.

idempotency key
pick a key and click Send
import { withIdempotency } from '@betternotify/core/middlewares';
import { inMemoryIdempotencyStore } from '@betternotify/core/stores';

const orderConfirmation = rpc
  .email()
  .input(z.object({ orderId: z.string(), email: z.string() }))
  .subject(({ input }) => `Order ${input.orderId} confirmed`)
  .template(adapter)
  .use(withIdempotency({
    store: inMemoryIdempotencyStore(),
    key: ({ input }) => `order:${input.orderId}`,
    ttl: 24 * 60 * 60_000,
  }));

Options

Prop

Type

Key design

The key is explicit by design — silently deduplicating payload-equal sends is rarely the intended behavior. Derive keys from business identifiers:

withIdempotency({
  store,
  key: ({ input }) => `invoice:${input.invoiceId}`,
  ttl: 60 * 60_000,
})

Two concurrent first-time sends with the same key will both run the pipeline. A future setIfAbsent addition to the store interface can close this gap.

On this page