The Inbound Dead Letter Queue Now Has a Viewer
Inbound deliveries that run out of retries, trip a circuit breaker or wait too long behind a throttle have always been written to a dead letter queue. Now you can open it: list entries with a classified reason, see why each one died, filter by route, retry them once the destination is healthy, or discard them.
A Queue You Couldn't Open
Every inbound delivery in Hookbase gets a fair number of chances. If the destination returns a 5xx or a 429, times out, or the connection fails, the delivery worker retries it with backoff (10s, 30s, 2 minutes, then 10 minutes between attempts), up to five attempts by default. If the route has failover destinations, those get tried too.
Some deliveries still don't make it. When that happened, the worker wrote a row to the dead letter queue, and that's where the trail ended. The only place the queue showed up was a small "N in DLQ" badge on the route node in the Architecture Diagram. It told you something had died. It didn't tell you what, why, or what to do about it.
That's fixed. Inbound → Dead Letter Queue is a new page in the dashboard, backed by a new set of API endpoints, that shows every dead-lettered inbound delivery and lets you act on it.
What Ends Up in the DLQ
A delivery is dead-lettered for one of five reasons, and the viewer classifies each entry into one of them:
| Reason | What happened | |---|---| | Max retries exceeded | Every retry attempt against the destination failed | | Failover exhausted | The primary destination and every failover destination failed | | Circuit breaker open | The route's circuit breaker had tripped, so the delivery was set aside instead of sent | | Throttled too long | The destination stayed throttled past the maximum hold time, and the delivery was abandoned | | Throttle queue full | The destination's throttle queue was at its configured depth, so the delivery was rejected for backpressure |
Anything the classifier doesn't recognize shows up as Other instead of being hidden.
What doesn't land here: a 4xx response other than 429. A 400 or 404 will fail the same way on every attempt, so Hookbase marks the delivery failed straight away and stops retrying. Those deliveries are still in your delivery history and can be replayed from there.
What the Viewer Shows
The top of the page has three summary cards:
- Total in DLQ, with the timestamp of the oldest entry, so you can tell a fresh incident from a backlog that has been sitting for weeks
- By reason, a count per reason code
- Top failing routes, the routes with the most dead-lettered deliveries
Below that is the list. Each entry shows:
- The event type and a color-coded reason badge
- How many attempts were made out of how many allowed (
5/5 attempts), and the last HTTP status the destination returned - The path the delivery took: source → route → destination, with the route linked to its settings page
- When it was dead-lettered, and a link to the original event so you can inspect the full payload and every delivery attempt
- The last error message, such as the destination's response body or the circuit breaker notice
- A Circuit open marker if the route's circuit breaker is still tripped
You can filter by route and by reason, and search by event ID or by text in the error message. The list pages with a cursor, 50 entries at a time, so it stays fast however large the queue gets.
The badge on the Architecture Diagram is now a link: clicking "N in DLQ" on a route node opens the viewer already filtered to that route.
Retry or Discard
Each entry has a Retry button and a discard button. You can also select several entries and retry or discard them together.
Retry queues a new delivery for the same event, route and destination, with a fresh set of five attempts, and removes the entry from the DLQ. The queue is meant to hold things that still need attention, and a retried entry no longer does. If the new delivery fails again, it gets its own DLQ entry. The retry is recorded in the audit log, and the new delivery shows up under the event like any other.
Some entries can't be retried, and the viewer tells you why instead of letting the click fail:
- Route is paused. Resume the route first, or the delivery would land straight back in the DLQ.
- Event removed by retention. The event has aged out of your retention window, so there's no payload left to send.
- Transient event. The source runs in transient mode, so the payload was never stored.
- Original delivery is gone. There's no destination to retry against.
When you retry a batch, the eligible entries are queued and the rest are skipped, and the response tells you which were skipped and why.
Discard permanently removes the DLQ entry. The delivery won't be retried. The original event and its delivery history are not touched.
Permissions
The viewer uses the same permissions as deliveries, so you don't need a new IAM policy:
- Viewing the queue and its stats needs
deliveries:read, which every role has, viewers included. - Retrying needs
deliveries:replay: members, admins and owners. - Discarding needs
deliveries:delete: admins and owners only, because it's the one action that can't be undone.
Retries and discards are both written to the audit log (dlq.retried and dlq.discarded), with the IDs involved.
Using the API
The viewer is built on endpoints you can call yourself, under /api/organizations/{orgId}/dlq:
| Method | Path | What it does |
|---|---|---|
| GET | / | List entries. Filters: routeId, sourceId, destinationId, reason, search, startDate, endDate. Paginate with cursor and limit (default 50, max 200) |
| GET | /stats | Total, oldest and newest entry, counts by reason, counts by route |
| POST | /{dlqId}/retry | Retry one entry |
| POST | /retry-bulk | Retry up to 100 entries: { "ids": [...] } |
| DELETE | /{dlqId} | Discard one entry |
| DELETE | /bulk | Discard up to 500 entries: { "ids": [...] } |
The reason filter takes the codes max_retries_exceeded, failover_exhausted, circuit_breaker_open, throttle_timeout, throttle_queue_full and other.
For example, after fixing a destination that was rejecting requests, you could pull the entries that exhausted their retries:
curl "https://api.hookbase.app/api/organizations/$ORG_ID/dlq?reason=max_retries_exceeded&limit=100" \
-H "Authorization: Bearer whr_your_api_key"
Each entry includes canRetry and, when retry isn't possible, retryBlockedReason. Collect the retryable IDs and send them back in one request:
curl -X POST "https://api.hookbase.app/api/organizations/$ORG_ID/dlq/retry-bulk" \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"ids": ["<dlq-entry-id>", "<dlq-entry-id>"]}'
The response reports how many entries were queued and skipped, and gives the reason for each skipped one in skippedDetail.
Note that this is the inbound dead letter queue: deliveries from your sources to your destinations. Outbound webhooks you send to your customers' endpoints have their own, separate dead letter queue in the outbound webhooks dashboard.
Getting Fewer Entries in the First Place
The DLQ is where you clean up after a failure. A few settings decide how much ends up there:
- Failover destinations give a delivery somewhere else to go before it's dead-lettered.
- Circuit breakers stop sending to a destination that keeps failing. Deliveries that arrive while the circuit is open are dead-lettered, and once the destination recovers you can retry them from the viewer.
- The production readiness guide covers how these fit together with alerting.
Try It
Open Inbound → Dead Letter Queue in the sidebar. If it's empty, every inbound delivery has either succeeded or is still retrying. If it isn't, start with the reason breakdown, fix whatever the destination was complaining about, and retry the batch.