Idempotency
Idempotency is the difference between "this event got delivered once" and "this event got delivered three times because the network blinked." Pushrail deduplicates at ingest using an idempotencyKey you provide (or that the SDKs generate for you). Use this on every event, it costs nothing and saves you from the long tail of double-billed customers and duplicate emails.
Why idempotency
Networks fail. Process supervisors restart. Lambda containers freeze mid-request and resume after the client has already retried. Without an idempotency key, every retry is a fresh event, and a webhook that triggered "send invoice email" runs N times instead of once.
With an idempotency key, the second request returns the same event id with status: "duplicate" and no new delivery is produced. Your retry logic stays simple, and the destination behavior stays correct.
The Idempotency-Key field
The canonical event envelope has an idempotencyKey field. The Node and Python SDKs auto-generate a UUIDv7 key per call unless you supply one, that handles the network-retry case automatically.
When you want dedup across re-runs (a migration, a queue redriver, an at-least-once worker), supply a deterministic key derived from the source-of-truth identifier:
await pushrail.events.emit({
eventType: "order.completed",
source: "checkout-service",
customerExternalId: row.customerId,
idempotencyKey: `order.completed:${row.id}`,
payload: { orderId: row.id },
});
The HTTP API also accepts an Idempotency-Key request header as an alternative to the body field; if both are present, the body field wins. Use the field for new code and the header only if you cannot modify the body shape.
Dedup window
The dedup window is 24 hours by default. An event with the same idempotencyKey (scoped to your tenant) accepted within that window collapses to a duplicate; outside the window, it is treated as a new event.
The window is long enough to cover network-level retries, daily migration re-runs, and most operator interventions. It is not long enough to cover "I forgot to deploy the script for two days." Pick a deterministic key derived from your domain identifiers when you need true long-horizon dedup.
The dedup window is scoped per tenant, not per eventType. Two different event types with the same idempotency key will collide. Namespace your keys (e.g., order.completed:ord_123 and order.refunded:ord_123) to avoid this.
What counts as a duplicate
A duplicate is "same idempotencyKey within the window, same tenant." Everything else about the event (payload, occurredAt, source) can differ; only the key is checked. The original event's payload is what gets persisted and delivered; the duplicate request's payload is discarded.
This matters when retries carry stale data. If your first attempt's payload was missing a field and you fixed it for the retry, the retry collapses to a duplicate and the missing-field version is what reaches destinations. Treat the first attempt's body as the source of truth and avoid mutating it across retries.
Duplicate detection happens at ingest, not at delivery. A successful ingest followed by a destination failure followed by a replay does not deduplicate at the delivery layer, that's what replay is for. See Replay & recovery for replay-side idempotency guidance.