Most brokers make a dead letter queue a convention you assemble from routing rules; Beanstalkd has it as one of three verbs.
$job = $queue->reserveWithTimeout(5);
try {
$this->handle($job->getData());
$queue->delete($job); // done
} catch (Transient $e) {
$queue->release($job, 1024, 60); // back to the tube in 60s
} catch (Throwable $e) {
$queue->bury($job); // set aside for a human
}
A buried job sits in its own state until somebody kicks it back, and kick takes a count so a batch can be retried after a fix. The distinction between release and bury maps directly onto transient and permanent failure, which is a distinction most handlers should be making anyway. A reserved job returns to the ready queue automatically when its time-to-run expires, so a crashed worker needs no recovery timer at all — which is the smallest correct queue I know of.