Retries and Failures
When a delivery doesn't succeed, HealthEx retries it on a fixed schedule that spans roughly 33 hours. This page defines exactly what counts as success, what we do on failure, and the two ways you can influence our retry behavior from your endpoint.
Successful Delivery
A delivery succeeds when your endpoint returns any 2xx status within 10 seconds. Nothing else is inspected — we don't read your response body, and we don't require a particular status code within the 2xx range.
Ten seconds is the whole budget: DNS, TLS handshake, request, and your response all have to fit inside it. That's a generous window for an acknowledgement and a tight one for real work, which leads to the single most useful piece of advice on this page:
Verify the signature, write the event somewhere durable, return 2xx, and do the actual processing outside the request. A handler that calls three internal services before replying will eventually exceed 10 seconds under load, and we'll treat those deliveries as failures and retry them — giving you duplicate work on top of a slow endpoint.
What Counts as a Failure
| Condition | Treatment |
|---|---|
| No response within 10 seconds | Retried |
3xx redirect | Retried. We do not follow redirects. |
4xx, including 429 Too Many Requests | Retried |
5xx | Retried |
| DNS resolution failure, TLS error, connection refused, network error | Retried |
410 Gone | Not retried. Pauses the webhook — see below. |
Two of these surprise people:
- Redirects are not followed. If your endpoint
301s to a canonical host, every delivery fails. Register the final URL directly. 4xxresponses are retried. We can't distinguish "this request is malformed" from "my auth layer is misconfigured and rejecting you", so we retry either way. If you return400because a signature failed to verify, expect eight attempts of it.
Retry Schedule
The schedule is fixed, not computed from a backoff formula. There are eight attempts in total: the initial delivery, then seven retries.
| After attempt | Next attempt in | Elapsed since event |
|---|---|---|
| 1 | 30 seconds | ~30 seconds |
| 2 | 2 minutes | ~2.5 minutes |
| 3 | 10 minutes | ~12.5 minutes |
| 4 | 30 minutes | ~43 minutes |
| 5 | 2 hours | ~2 hours 43 minutes |
| 6 | 6 hours | ~8 hours 43 minutes |
| 7 | 24 hours | ~32 hours 43 minutes |
| 8 | — | Permanently failed |
Each delay gets up to 10% of random jitter added — never subtracted — so intervals are slightly longer than the table and simultaneous failures across many webhooks don't retry in lockstep. Elapsed times are therefore approximate, and each attempt can pick up a few extra seconds of queue latency.
Every attempt carries the same webhook-id and the same body, including the body's timestamp. The deliveryAttempt body field increments, and the webhook-timestamp header is regenerated per attempt.
Slowing Us Down
If you return a Retry-After header on a failed delivery, we honor it as a floor on the next delay. It can push a retry further out but never bring it forward.
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
Both forms are accepted: an integer number of seconds, or an HTTP-date. The next attempt is scheduled at whichever is later — your requested delay, or the position in the fixed schedule.
This is worth using if you're rate-limited or in a known maintenance window. It doesn't consume extra attempts; it just spaces them out.
Telling Us to Stop
Return 410 Gone when an endpoint is permanently retired. We treat it as a definitive instruction:
- The delivery is not retried.
- The webhook's status changes to Paused (auto) immediately.
- No further events are delivered until someone re-enables it in the Admin console.
410 isn't the only automatic trigger, though: a webhook is also paused automatically once 20 consecutive deliveries permanently fail — 20 events in a row that each exhaust all eight retry attempts without a single one succeeding. A run of 500s or timeouts within one event's retries doesn't pause anything by itself; it takes 20 straight events failing all the way through, with no successful delivery landing in between, to trigger it.
410 is not a "back off" signal — it's a "stop forever" signal, and it takes one response to trigger. If a misconfigured route or an overzealous framework default returns 410 during an outage, your webhook is paused and every event that occurs before you notice is discarded, not queued. Use 503 with a Retry-After for transient conditions.
Permanent Failure
If all eight attempts fail, we stop trying and mark the delivery permanently failed. Three consequences:
- The webhook stays Active. Exhausting the retries for one event has no effect on the next one, which starts its own fresh chain of eight attempts.
- You are not notified. There is no email or callback on permanent failure. The delivery log is where it shows up.
- The record is retained for 30 days and can be replayed manually from the delivery log.
For consent.withdrawn specifically, a permanent failure also alerts HealthEx Support, who will contact you directly so the withdrawal is honored by other means. That is a backstop for a legally significant event, not a routine part of the delivery path.
Paused Webhooks
While a webhook is paused — whether you paused it, a 410 did, or 20 consecutive deliveries permanently failed — matching events are discarded.
There is no buffer and no backfill. Resuming a webhook does not deliver anything that occurred while it was paused, and those events cannot be recovered afterwards. This applies equally to Paused (manual) and Paused (auto).
If you need deliveries to stop briefly without losing events, let your endpoint fail instead of pausing the webhook. The retry schedule holds each event for roughly 33 hours, which covers most planned maintenance.
Test events are the one exception: Send test event delivers even to a paused webhook, so you can confirm an endpoint is healthy again before re-enabling it.
Next Steps
- Idempotency and Ordering: Retries mean duplicates — here's how to handle them
- Troubleshooting: Inspect real attempts and replay a failed delivery