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.
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:
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
endThe 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:
class FragileApiJob < ApplicationJob
include Pgbus::RetryBackoff::JobMixin
pgbus_retry_backoff base: 10, max: 600, jitter: 0.2
def perform(...)
# ...
end
endThe 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.
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