Skip to content

Delivery, retries & idempotency

Ingestion is at-least-once: a timeout can mean the server received your batch and the response was lost. The contract below is what every Whisperr SDK implements — if you call the API directly, implement the same outcomes.

Response Classification What to do
2xx ok Delivered. Remove the batch from your queue.
401 / 403 auth Your key is wrong or revoked. Stop sending, surface the error, and keep the batch queued — retrying won’t help until the key is fixed.
429, 5xx, network error, timeout retry Transient. Retry with bounded exponential backoff; if retries are exhausted, keep the batch queued for a later attempt rather than dropping it.
any other 4xx drop The payload is malformed and will never succeed. Drop the batch, and log it — this is a bug in your integration, not a transient fault.

Two asymmetries worth internalizing:

  • Auth errors and exhausted retries retain; malformed drops. Transient and auth failures are recoverable, so the data is kept. A 400 is deterministic — resending identical bytes fails identically, so retrying only burns quota.
  • A single malformed event can 400 a whole batch (the API rejects unknown fields and bad event_type names). Validate events before they enter your queue so one bad event can’t wedge the pipeline.

Every event’s context must contain $message_id — a per-event idempotency key (any stable unique string; UUID recommended). The server deduplicates on it, which is what makes at-least-once retries safe.

The rule: generate $message_id once, when the event is created — never per attempt. Retries of the same event must send the identical id. If you regenerate it on retry, every timeout becomes a potential duplicate event.

create event ──▶ assign $message_id ──▶ enqueue
send ◀── retry ◀─────┤ same $message_id every attempt
2xx ──▶ dequeue

Use bounded exponential backoff for the retry class (429/5xx/network). The SDKs default to ~6 attempts with a 10s per-request timeout; on a 429, honor Retry-After if present. After exhausting retries, park the batch and try again on your next flush cycle instead of dropping it.

This contract is pinned by executable fixtures in the whisperr-spec repository (conformance/wire.json for serialized bodies, conformance/behavior.json for retry/drop/retain outcomes). Every official SDK runs against them in CI — if you’re building your own client, they’re the ground truth.