Retries & idempotency
How Kwill retries failed deliveries and how to handle duplicate events safely.
Engineer review required before merge (SCRUM-974 AC). Confirm that the retry schedule and auto-disable threshold below match src/lib/webhooks/deliver.ts at review time.
Delivery expectations
Kwill considers a delivery successful when your endpoint returns a 2xx status code within 10 seconds. Any non-2xx response, connection error, or timeout is treated as a failure and triggers a retry.
Return 2xx immediately and process the event asynchronously. If your handler is slow (database writes, external API calls), it will time out and the event will be retried.
Retry schedule
Failed deliveries are retried up to 7 total attempts (1 initial + 6 retries) over approximately 33 hours:
| Attempt | Wait before this retry |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 24 hours |
After all 7 attempts are exhausted, the delivery is marked failed and no further retries occur.
Auto-disable
If 10 consecutive deliveries to an endpoint are exhausted (all 7 attempts failed), the endpoint is automatically disabled. You will need to re-enable it manually in the dashboard (Settings → Developer → Webhooks).
Fix the issue with your endpoint before re-enabling — otherwise it will be disabled again quickly.
Idempotency
Kwill guarantees at-least-once delivery. In rare cases (network issues, timeout races), you may receive the same event more than once.
Use X-Kwill-Event-Id to deduplicate. Store processed event IDs and skip any event whose ID you have already handled:
// Express + some hypothetical DB client
app.post('/webhooks/kwill', async (req, res) => {
// ... verify signature first ...
const eventId = req.headers['x-kwill-event-id'];
const event = JSON.parse(req.body);
const alreadyProcessed = await db.processedEvents.exists(eventId);
if (alreadyProcessed) {
return res.sendStatus(200); // acknowledge without re-processing
}
await db.processedEvents.insert(eventId);
// handle event.type ...
res.sendStatus(200);
});Delivery history
The dashboard shows the full delivery log for each event — attempt number, response code, response body, and timing. Use this to debug failures without needing to reproduce them.
Test events
webhook.test events (sent via the dashboard or POST /webhooks/{id}/test) get exactly one attempt. A failure is final and does not count against the endpoint's consecutive failure streak.