Guide

# Retries & dead letters

Failures retry with exponential backoff, then route to a dead-letter queue instead of spinning forever.

## Retries ride the visibility timeout

pgbus doesn't re-enqueue a failed job. When a job raises, the worker lets the PGMQ **visibility timeout** expire — the message simply becomes visible again and another worker picks it up. PGMQ's `read_ct` counts each delivery, so pgbus always knows how many attempts a message has had.

> **Note:** This is why there's no separate retry table: the queue itself is the retry mechanism. A message that fails is never lost — it's just invisible until its timeout lapses.

## Exponential backoff with jitter

Spread retries out instead of bunching them.

Rather than retry at a fixed interval, pgbus extends the visibility timeout with exponential backoff and a little jitter, so a thundering herd of failures doesn't all retry at the same instant:

```ruby
Pgbus.configure do |config|
  config.retry_backoff        = 5    # base delay (seconds)
  config.retry_backoff_max    = 300  # cap at 5 minutes
  config.retry_backoff_jitter = 0.15 # ±15% randomization
end
```

The delay is `base * 2^(attempt-1) * (1 + jitter)`. With the defaults, a job that fails four times waits roughly 5s, 10s, 20s, 40s before it hits the DLQ on the fifth read.

## Per-job overrides

A fragile job — say one calling a flaky third-party API — can widen its own backoff without changing the global setting:

```ruby
class FragileApiJob < ApplicationJob
  include Pgbus::RetryBackoff::JobMixin

  pgbus_retry_backoff base: 10, max: 600, jitter: 0.2

  def perform(...)
    # ...
  end
end
```

## The dead-letter queue

read_ct > max_retries → <queue>_dlq.

When `read_ct` exceeds `max_retries` (default 5), the message stops retrying and moves to a dead-letter queue named `<queue>_dlq`. It sits there for inspection instead of consuming worker capacity forever.

send_message

→ q_<queue>

read (worker)

vt: message hidden

read_batch

archive → a_<queue>

success

perform_now ok

raise → vt expires, read_ct++

<queue>_dlq

read_ct > max_retries

give up

Success archives the message; repeated failures raise read_ct until it crosses max_retries and routes to the DLQ.

Dead-lettered messages show up in the [dashboard](https://pgbus.zoolutions.llc/docs/dashboard), where you can inspect the payload and the failure. Tune the threshold with `max_retries`:

```ruby
Pgbus.configure { |c| c.max_retries = 3 } # DLQ after 3 failed reads
```