Delivery and retries
What counts as delivered
Any 2xx within 10 seconds. Anything else (a 4xx, a 5xx, a timeout, a connection failure)
is a failed attempt and will be retried.
Return 2xx as soon as you have safely stored the event. Do not process it, call other
systems, or update a UI before responding: that work belongs after the acknowledgement. A receiver
that does its processing inline will start timing out under load, and timeouts become retries,
which become duplicates.
Retry schedule
Up to 5 attempts, backing off 1, 5, 15 and 60 minutes after the first failure.
After the final attempt the delivery stops. It is not retried again, and it is not queued indefinitely, so a receiver that is down for more than about eighty minutes will miss events permanently. If you need to recover, subscription status tells you the current truth for a number.
At-least-once, never exactly-once
The same event can arrive more than once. This is not a defect to be fixed: it is the guarantee. A delivery that succeeded on your side but whose response we never saw will be retried.
Your receiver must be idempotent. Use eventId:
if already_processed(eventId): return 200
process(event)
mark_processed(eventId)
return 200
Getting this wrong is expensive in a specific way: a duplicate subscription.created that
grants content twice, or a duplicate charge_failed that sends two dunning messages, both look
like our bug and are not.
Ordering is not guaranteed
Events are delivered independently. A retry can arrive after a later event has already been
processed: a subscription.created landing after the subscription.renewed that followed it.
Use occurredAt to order, not arrival time, and make your handlers tolerant of arriving out of
sequence.
When deliveries keep failing
Repeated failures are visible to the operator team, who can tell you what we saw: the status code,
the timing, and how many attempts were made. Quote the eventId and it can be traced to a
specific delivery.