Why "At Least Once" Is the Default Contract

When a callbot platform fires webhooks for events like call completion, agent handoff, or DTMF input, most providers guarantee "at-least-once" delivery rather than "exactly once." Twilio resends the same status callback after a connection failure or a response timeout, and the call events Amazon Connect streams through EventBridge get re-queued for retry whenever the target invocation fails. If the receiving side is built on the assumption that each event arrives exactly once, a single retry turns straight into duplicate processing and duplicate billing.

The Two Lines Where EventBridge Retries Stop

Amazon EventBridge's retry policy stops at whichever of two limits is hit first: MaxRetryAttempts (0 to 185, default 5) and MaxEventAgeInSeconds (60 to 86,400, default 300). Raise the attempt count to 185 but leave the event age at its 300-second default, and retries only fire for failures that repeat within five minutes before the event drops straight to the dead-letter queue. Stretch the age window to 24 hours instead, and the same event gets a longer, more frequent window to re-arrive — widening the retry window is the same move as widening the duplicate-delivery window.

Field Guide: A Call-End Webhook Idempotency Checklist

Declare the idempotency targets as numbers before writing any code: zero duplicate-billing incidents from repeated events, a 100% detection rate for idempotency-key collisions, and a p99 response time under 300ms for the webhook handler's 2xx reply. Set the idempotency-key store's TTL longer than the MaxEventAgeInSeconds you configured for retries — if you're running a 24-hour window, give the TTL at least 48 hours of headroom.

A common failure pattern: the handler finishes transcription and CRM updates synchronously before replying, the processing time creeps past the response timeout, and the provider treats that as a failure and resends the same event. Returning a 202 immediately and pushing the actual work onto a queue cuts down on this class of retry on its own.

A second failure pattern: deduplicating on call_sid alone collapses genuinely different events from the same call — completion, hold, and transfer — into one. Build the idempotency key from call_sid plus event_type plus a timestamp bucket so each event type gets deduplicated independently.

Before shipping, reproduce a resend scenario where the same event is sent three times in a row. Log four fields on every delivery — receipt time, attempt count, the idempotency key, and the dedup verdict — so a retry pattern can be reconstructed after the fact. Before manually reprocessing anything sitting in the dead-letter queue, check why it wasn't handled within the 185-attempt budget first — a downstream outage and an idempotency-key collision call for different fixes.

When an idempotency-key collision is detected, skip processing the second request and return the first request's stored result as-is, so the caller still sees success. If processing genuinely failed and the event landed in the dead-letter queue, fire an alert immediately and hold it in a reprocessing queue so a human approval re-injects it right away.

Track the duplicate-delivery rate (the share of events with dedup=true) and dead-letter counts weekly. A rising duplicate rate points at the provider's timeout settings or the network path; a growing dead-letter queue points at a retry window or downstream capacity that needs retuning.

Takeaways at a Glance

Call-event webhooks in a callbot loop have to start from the assumption that at-least-once delivery is the baseline, not the exception. An idempotency key built from call_sid and event_type, a TTL longer than the retry window, an immediate-202 response structure, and a tested resend scenario together mean retries can run all the way to attempt 185 without duplicate billing or state corruption.

References

Event retry policy and using dead-letter queues — Amazon EventBridge

Webhooks (HTTP callbacks): Connection Overrides — Twilio

Ask AI about this article

The assistant has read this article. Ask anything — it answers from the text and says so when something isn't in it.

Loading the chat…