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 handling
Section titled “Response handling”| 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
400is 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_typenames). Validate events before they enter your queue so one bad event can’t wedge the pipeline.
Idempotency: $message_id
Section titled “Idempotency: $message_id”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 ──▶ dequeueBackoff
Section titled “Backoff”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.
Conformance
Section titled “Conformance”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.