Retry Logic
Understand how Mail.Land retries failed webhook deliveries
When a webhook delivery fails, MailLand automatically retries with increasing delays. This page covers the retry schedule, delivery statuses, failure handling, and how to monitor and retry deliveries manually.
Retry Schedule
This section covers every event delivered by the durable dispatcher: message.*, suppression.*,
account.* and inbound.received. See Inbound Deliveries below for what is specific
to inbound events.
MailLand retries a failed delivery up to 6 times in total, with an increasing delay before each retry:
| Attempt | Delay before this attempt | Total Elapsed |
|---|---|---|
| 1 | : (initial attempt) | 0 |
| 2 | Immediate | ~0 |
| 3 | 1 minute | ~1 minute |
| 4 | 5 minutes | ~6 minutes |
| 5 | 15 minutes | ~21 minutes |
| 6 | 1 hour | ~1 hour 21 minutes |
After the 6th attempt fails, the delivery is marked as permanently failed: there is no further wait and no 7th attempt.
Retryable vs Non-Retryable
Retryable responses
These status codes trigger automatic retries:
408Request Timeout429Too Many Requests500Internal Server Error502Bad Gateway503Service Unavailable504Gateway Timeout- Network errors (timeouts, socket errors, DNS failures)
Non-retryable responses
These status codes are treated as permanent failures, no retry:
400Bad Request401Unauthorized403Forbidden404Not Found- All other
4xxerrors (except 408 and 429) - A redirect response that fails validation (see Redirects) or exceeds the hop limit
If your endpoint returns a Retry-After header on a retryable response, MailLand uses that value
as the delay before the next attempt instead of the schedule above: for any retryable status, not
only 429. There is no upper cap and no jitter applied to it: honor this on your side too if you
send it, since a very large value is honored exactly as given.
Redirects
MailLand does not treat your endpoint as a browser would. Redirects are followed manually, at most 3 hops, and every hop is re-validated before anything is sent to it:
- the target must be
https://: a redirect tohttp://is refused, not downgraded; - the target must resolve to a public address: a redirect to a private, loopback or link-local address is refused, and the request is never issued;
- exceeding 3 hops fails the delivery.
A refused redirect is reported as a delivery failure with no status code. If your endpoint moved, point the webhook at the new URL rather than relying on a redirect.
Delivery Configuration
| Setting | Default | Description |
|---|---|---|
| HTTP timeout | 15 seconds | Max wait time per delivery attempt |
| Max retries | 6 attempts | Total delivery attempts including the initial one |
| Auto-pause threshold | 10 consecutive failures | Webhook is paused after this many failures in a row: uses the webhook's own pauseThreshold when set to a value greater than 0, otherwise this default |
| Deduplication | While the delivery record exists | Duplicates of the same event are suppressed by the (WebhookId, EventId) constraint for as long as the row exists: at least 30 days for delivered deliveries |
Inbound Deliveries
inbound.received is delivered by the same durable dispatcher as every other event, with the same retry
schedule, Retry-After handling, delivery history and auto-pause described above.
- Deduplicate on
event_id(equal toX-MLND-Event-Id), like any other event. It is stable per inbound message and webhook, so every attempt for the same email carries the same id. - A manual replay is a new event: a new
event_id, with the original one indata.replay_of.data.idempotency_keystays the same. - Each attachment's
download_urlis signed for each attempt and is valid for 15 minutes from then; the attachment'sidis the same on every attempt. See Inbound Webhooks.
View Delivery History
See past deliveries for a webhook:
GET https://api.mail.land/v1/webhooks/{id}/deliveriescurl -X GET "https://api.mail.land/v1/webhooks/wh_abc123/deliveries?limit=20" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | - | Opaque cursor from the previous page's nextCursor. Omit for the first page. |
limit | int | 20 | Items per page |
status | string | - | Filter: success or failed |
eventType | string | - | Filter by event type (e.g., message.delivered) |
from | datetime | - | Start date |
to | datetime | - | End date |
{
"items": [
{
"publicId": "p8r2kx7m9wq4rtz",
"eventType": "message.delivered",
"messageId": "t_a1b2c3d4e5f6",
"statusCode": 200,
"responseTimeMs": 145,
"success": true,
"errorMessage": null,
"createdAt": "2026-02-17T12:34:58.000Z",
"attemptNumber": 1
},
{
"publicId": "p8r2kx7m3nq1abc",
"eventType": "message.bounced",
"messageId": "t_d4e5f6a1b2c3",
"statusCode": 503,
"responseTimeMs": 15000,
"success": false,
"errorMessage": "Service Unavailable",
"createdAt": "2026-02-17T12:30:00.000Z",
"attemptNumber": 3
}
],
"nextCursor": "eyJ2IjoxLCJzIjoiY3JlYXRlZGF0OmRlc2MiLCJrIjoi...",
"hasMore": true
}Retry a Failed Delivery
Manually retry a specific failed delivery. The retry carries the same event_id and body as the
original attempt, only X-MLND-Attempt and the signature timestamp differ, so your handler should
deduplicate on event_id rather than assume a second delivery is a second event:
POST https://api.mail.land/v1/webhooks/{id}/deliveries/{publicId}/retrycurl -X POST https://api.mail.land/v1/webhooks/wh_abc123/deliveries/p8r2kx7m3nq1abc/retry \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Returns 202 Accepted on success. The retry is queued and uses the original event payload.
| Status | Code | Description |
|---|---|---|
202 | - | Retry queued successfully |
202 | - | Delivery is already queued for retry, no-op, not an error |
400 | ALREADY_SUCCEEDED | Cannot retry a delivery that already succeeded |
400 | INVALID_REQUEST | Delivery predates durable retry tracking, or is currently in flight |
404 | WEBHOOK_NOT_FOUND | Webhook does not exist |
404 | DELIVERY_NOT_FOUND | Delivery ID does not exist |
Auto-Pause on Failures
When a webhook accumulates enough consecutive failed delivery attempts, MailLand automatically
pauses it. The threshold is the webhook's own pauseThreshold when it is set to a value greater
than 0, otherwise the default of 10 consecutive failed delivery attempts:
- The webhook's
isActiveis set tofalse, andpausedAtandpausedReasonare populated:pausedReasontells you how many failures and what the last one was - An in-app notification is created
- No further events are delivered until the webhook is resumed
The counter is of attempts, not of events: one event that exhausts its retry ladder contributes several failures.
There is no earlier warning notification before the threshold is hit: check consecutiveFailures
proactively (see Best Practices below) rather than waiting to be told.
Resume a Paused Webhook
POST https://api.mail.land/v1/webhooks/{id}/resumecurl -X POST https://api.mail.land/v1/webhooks/wh_abc123/resume \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."This reactivates the webhook: it clears the pause state, resets the consecutive failure counter to zero, and sets it active again. The next event delivery will be attempted normally.
Any successful delivery automatically resets the consecutive failure counter to zero. A single success after 9 failures prevents the webhook from being paused.
Best Practices
- Return 200 quickly: process webhook payloads asynchronously. Queue the event and return
200immediately. Long-running handlers risk timeouts. - Use the event ID for idempotent consumption: the
X-MLND-Event-Idheader (and the matchingevent_idin the payload) is stable across retries and replays: the same underlying event always arrives with the same id, no matter how many times it is redelivered. Store processed IDs and skip ones you have already handled, so a duplicate delivery is a no-op on your side. - Monitor consecutive failures: check the
consecutiveFailuresfield when listing webhooks. Investigate before hitting the auto-pause threshold. - Handle 429 properly: if your endpoint is overloaded, return
429with aRetry-Afterheader. MailLand will respect it.
Next Steps
- Signature Verification: verify webhook authenticity
- Events: complete event type reference
- Webhooks Overview: create and manage webhooks