Skip to main content

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.