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.

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:

config/initializers/pgbus.rb
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:

app/jobs/fragile_api_job.rb
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.

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, where you can inspect the payload and the failure. Tune the threshold with max_retries:

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