Every durable agent system eventually needs an answer to one question: when a task has failed for the fifth time, where does it go? Once you've moved agent work off the request path and onto a queue you poll for long-running results, this becomes unavoidable. The reflex answer is a dead-letter queue — a settled, forty-year-old pattern with first-class support in every message broker. The reflex is right that you need somewhere for doomed work to land. It is wrong about what happens when it gets there, because the DLQ was designed for a unit of work an agent task is not.
The pattern, and the assumption underneath it#
A dead-letter queue exists to stop a poison message — one that fails processing every time — from being redelivered forever. AWS SQS implements this with a redrive policy: after a message has been received maxReceiveCount times (you set it anywhere from 1 to 1,000) without being deleted, SQS moves it to the DLQ. Azure Service Bus does the same once delivery count exceeds MaxDeliveryCount (default 10). Later, when you've fixed the bug, you redrive: move the parked messages back to the source queue and let them reprocess.
Notice the assumption threaded through all of it: the unit of work is a static payload, and processing it is deterministic. Redelivery is safe to reason about because the bytes are identical each time and the handler behaves the same way. Fix the handler, replay the bytes, get success. That is the entire mental model, and an agent task breaks it in two places.
Break one: redrive is a fresh sample, not a retry#
An agent task is not a payload. It is a non-deterministic multi-step trajectory — a sequence of model calls, tool invocations, and branches that will not necessarily be the same twice. So "redrive" does not mean what it means for a message.
Replaying a message re-runs the same function on the same bytes. Replaying an agent task rolls the dice again.
When you redrive a failed SQS message, you are re-executing a deterministic handler; if you fixed the cause, it succeeds because nothing else changed. When you re-run a failed agent task, you draw a new sample from a stochastic process. It may take a path it never took before and succeed — or fail — for reasons that have nothing to do with any fix you shipped. This is the trap in "it worked on retry": sometimes that's a real fix, and sometimes it's variance, and the DLQ redrive button cannot tell you which. The practical consequence is that a blind redrive of an agent task is either wasteful or non-reproducible. Redrive should be gated on an actual change — a corrected prompt, a repaired tool, a different input — or on a human, not offered as a one-click replay.
Break two: a poison agent task has unbounded cost#
A classic poison message is annoying but cheap: each failed delivery wastes one fixed handler execution. A poison agent task is a different animal. It can loop — re-planning, re-calling tools, re-reasoning — and burn thousands of tokens per attempt before it finally fails or gets killed. Multiply that by a retry count of 5 or 10 and a single stuck task becomes a line item.
That changes what the failure threshold is for. In a message system, maxReceiveCount is queue hygiene. In an agent system, the equivalent threshold is a spend circuit-breaker, and receive count is the wrong meter — you want to bound tokens and steps, not just how many times the task was picked up. Platforms are converging on the retry-then-handoff shape: Inngest retries four times after the first attempt (five total) and then fires an onFailure handler; the agent version of that handler needs to look at how much the task spent, not only that it failed.
What actually goes in the record#
Put the two breaks together and the design falls out. An agent dead-letter record cannot be just the input payload a message DLQ would keep — you can't diagnose a stochastic failure from its input. It has to capture the trajectory (what the agent actually did, step by step) and the spend (tokens and steps consumed), alongside the input. The trajectory is what makes the failure debuggable; the spend is what makes the threshold a budget.
And one old rule gets promoted from best-practice to mandatory. Because queues deliver at-least-once and re-execution is stochastic, the odds that a redriven task double-runs a side effect are higher than in any deterministic system. AWS has preached idempotent consumers for message queues for years; for agents that take real-world actions, an idempotency key on every side-effecting tool call is the only thing standing between a redrive and a customer getting charged twice.
Durable-execution engines like Temporal sidestep some of this — persisted state plus NonRetryableErrorTypes means a permanent failure surfaces as an inspectable failed Workflow rather than a message in a side queue. But if your agents ride on a plain queue or job runner that still assumes deterministic handlers, the DLQ you inherit is quietly making three wrong assumptions. Fix them before a poison task finds them for you.



