Skip to main content

Delivery & retries

Orchestr delivers each event to every subscribed endpoint and retries failed deliveries on a fixed backoff schedule.

What counts as success

A delivery succeeds when your endpoint returns an HTTP 2xx status within the timeouts below.

LimitValue
Connect timeout3 seconds
Read timeout30 seconds

Anything else is a failure and will be retried, including:

  • any non-2xx status (a 4xx is retried too — your endpoint may be temporarily misconfigured and fixed before attempts run out);
  • connection refused, DNS failure, or TLS errors;
  • a response that exceeds the read timeout.

Retry schedule

A delivery is attempted up to 6 times (1 initial attempt + 5 retries). After each failed attempt, Orchestr waits before the next one:

After failed attemptWait before next attempt
11 minute
25 minutes
330 minutes
42 hours
524 hours
6— (no further retries)

The total retry window spans roughly 27 hours from the first attempt. The first attempt is made immediately when the event is produced; retries are dispatched by a poller shortly after each backoff interval elapses.

Delivery states

Each delivery moves through these states:

StateMeaning
PENDINGCreated, awaiting the first attempt.
PROCESSINGCurrently being delivered.
SUCCEEDEDYour endpoint returned 2xx. Terminal.
FAILEDAn attempt failed; a retry is scheduled.
EXHAUSTEDAll attempts failed. Terminal — no further delivery.
INVALIDThe event could not be delivered (e.g. the endpoint URL failed validation at send time). Terminal.

When a delivery reaches EXHAUSTED, Orchestr stops trying. Make sure your endpoint is reliable, and reconcile against the API (for example by searching transactions or orders) if you suspect missed events.

Idempotency

The same event can be delivered more than once — for example when your endpoint returns 2xx but the connection drops before Orchestr records the success, or when several endpoints are subscribed. Design handlers to be idempotent:

  • De-duplicate on the envelope id. Record processed event IDs and ignore repeats.
  • Make the side effects of processing safe to apply at-least-once (upserts, conditional updates).
  • Do not assume events arrive in order. Use event_time / created and the resource status to decide whether an event is newer than what you have already applied.
on receive(event):
if seen(event.id): # already processed → ack and stop
return 200
verify_signature(event) # reject if it does not verify
persist(event.id) # mark as seen
enqueue_for_processing(event) # do slow work asynchronously
return 200