Skip to content
Documentation menuIdempotency

Idempotency. One event, once.

The Tracking API stores each messageId + timestamp pair once. Give the same real-world event the same id every time, and retries, duplicate webhooks and re-run jobs never count it twice.

Generate deterministic ids

Derive the id from the real event, not at random: purchase:<order number>, trial_started:<account id>. If an event can be emitted twice, send the business time (when the payment was received) as timestamp, not “now”.

messageId is at most 100 printable ASCII characters. Without one the server derives it from a hash of the content, and the server SDK gives a missing id a random UUID; do not rely on either for business events.

@metrics/node · TypeScript
import { deterministicMessageId } from '@metrics/node';

deterministicMessageId('purchase_completed', order.id);   // one purchase per order
deterministicMessageId('subscription_renewed', sub.id, period.start.toISOString());

The answer to a duplicate

  • A duplicate counts as accepted (200, within accepted) and is stored once.
  • Do not resend a rejected message (rejected[]) unchanged; it is rejected again.
  • On 5xx and network errors, retry with exponential backoff and the same messageId.
  • 429 (rate_limited) is the rate limit, and nothing of the batch was stored: wait the number of seconds in the Retry-After header, then resend the same batch with the same messageIds.

Events that must not be lost

Write sign-ups, purchases and renewals in the same database transaction as the business change: @metrics/node/outbox-pg adds the event to the outbox with ON CONFLICT (message_id) DO NOTHING, and the dispatcher does the rest.