Delivery & Retries
How Webbu delivers batches, what counts as a failure and how retries work.
Delivery flow
When a flush condition is met, Webbu:
- Locks the group so only one worker delivers it
- Loads the items, oldest first, up to
batch.maxItems - Sends one request to
delivery.url - On success, removes the delivered items and records metrics
- On failure, schedules a retry or moves the batch to the dead letter queue
Request format
POST /webhooks/batch HTTP/1.1
Content-Type: application/json
x-idempotency-key: grp:b_orders:9a1c0b7e3d5f2a48:0:v3delivery.method can be POST (default) or PUT. Any delivery.headers you configured are added, and a signature header if request signing is enabled.
Body:
{
"webbu": {
"customerId": "c_your_account",
"projectId": "p_shop",
"bufferId": "b_orders",
"groupKey": "cus_001",
"groupHash": "9a1c0b7e3d5f2a48",
"segment": 0
},
"window": {
"firstItemAt": 1767225600000,
"lastActivityAt": 1767225604000,
"itemsCount": 2
},
"items": [
{ "messageId": "5b0c6f0e-…", "receivedAt": 1767225600000, "payload": { "event": "order.created" } },
{ "messageId": "c2d8e1a0-…", "receivedAt": 1767225604000, "payload": { "event": "order.paid" } }
]
}| Field | Description |
|---|---|
webbu | Where the batch comes from: account, project, buffer, group key and segment |
window.firstItemAt / lastActivityAt | When the group received its first and its latest item (epoch ms) |
window.itemsCount | Number of items in this request |
items[].messageId | The item's message ID (from messageId in the body, X-Idempotency-Key, or generated) |
items[].receivedAt | When Webbu accepted the item (epoch ms) |
items[].payload | The JSON body exactly as Webbu parsed it. The original request headers are not forwarded |
Success and failure
A delivery succeeds when your endpoint returns any 2xx status within 10 seconds. The timeout is fixed.
| Result | What Webbu does |
|---|---|
2xx | Success: items are removed from the buffer |
408, 429 | Retry with backoff |
5xx | Retry with backoff |
| Timeout (10 s), connection refused or reset | Retry with backoff |
Other 4xx (e.g. 400, 401, 404) | No retry: moved to the DLQ right away |
3xx redirects | Not followed and not retried: moved to the DLQ |
| Other network errors (e.g. DNS or TLS failures) | No retry: moved to the DLQ |
Retries and backoff
throughput.maxAttempts is the total number of attempts, including the first (default 3, maximum 5). The delay before retry n is:
delay(n) = min(initialMs × multiplier^(n−1), maxMs)With the defaults (initialMs: 1000, multiplier: 2, maxMs: 60000):
| Attempt | When |
|---|---|
| 1 | When the flush condition is met |
| 2 | 1 s after attempt 1 fails |
| 3 | 2 s after attempt 2 fails |
| (DLQ) | After attempt 3 fails |
With maxAttempts: 5 and the same backoff, the retries wait 1, 2, 4 and 8 seconds. Raise initialMs (for example to 30000) if your endpoint needs more time to recover.
Each retry loads the group's items again, so a retry can include items that arrived after the first attempt.
Idempotency
The idempotency header (default x-idempotency-key, configurable as delivery.idempotencyHeader) has the form grp:{groupId}:v{n}. It stays the same across retries of the same flush and changes after the group is flushed.
Delivery is at least once: if your endpoint processes a batch but answers too late, the batch is sent again. Make processing idempotent, ideally per item using items[].messageId.
Recommendations for your endpoint
- Acknowledge quickly: return
2xxwithin 10 seconds and process asynchronously if needed - Be idempotent: deduplicate by
items[].messageId - Accept large bodies: a batch can hold up to
maxItemspayloads - Use status codes deliberately: return
5xxor429for temporary problems (retried) and4xxonly for requests that will never succeed (not retried)