Retry & Dead-letter Queues

A function call failed — a Kafka handler, an SQS consumer, any func(args). Hand rdq the handler and its payload; rdq retries it on your schedule, or parks it in a dead-letter queue with its full failure history, ready to inspect and redrive.

Get started   ★ Star on GitHub

Bring-your-own-storage · broker-agnostic · polyglot · at-least-once · horizontally scalable

Embed the Go or Java SDK, or run rdq-server as a central retry hub for any language.

🗄️ Bring your own storage

Retry queues and DLQs live in the datastore you already run — PostgreSQL first, more via a documented SPI. No new stateful infra.

🔌 Broker-agnostic

Kafka, SQS, Redpanda, RabbitMQ, or no broker at all. rdq accepts failures from any source and never sits on the happy path.

⚰️ DLQ as a product

Every attempt's error, stack trace, and timestamp travels into the DLQ — browse, filter, and safely single- or bulk-redrive.

📈 Scales sideways

Stateless workers, atomic claims, and leases. Add a node to scale; a kill -9 mid-task is a non-event.

How rdq fits

rdq is not a broker and not on the happy path. A failed call is submitted to storage you already run; stateless workers claim due tasks, re-invoke the handler on your backoff schedule, and park exhausted work in the DLQ — inspectable and replayable.

Your app / consumer Kafka · SQS · RabbitMQ or any func(args) rdq engine stateless workers claim · retry · lease Handler in-process fn, or HTTP / gRPC callback Your storage — PostgreSQL retry queue DLQ rdq CLI / API browse · redrive · purge submit invoke fail → backoff claim / record (atomic) redrive
// Register a handler under a stable name — it survives
// deploys, restarts, and language boundaries.
rdq.Register("charge-payment", func(ctx context.Context,
    t envelope.Envelope) error {
    if err := charge(t.Payload()); err != nil {
        return err   // non-nil → retried on your backoff schedule
    }
    return nil       // nil → SUCCEEDED
})

// A worker claims due tasks atomically, invokes the
// handler, and records every attempt.
w, _ := rdq.NewWorker(store, rdq.WithQueue("payments.charge",
    rdq.MaxAttempts(5),
    rdq.BackoffExponential(time.Second, 2.0, 5*time.Minute),
))
w.Run(ctx)

Embed the SDK — zero extra infra

Register handlers under stable names, wrap an existing consumer, and let the worker retry with backoff and dead-letter with full context. The only dependency is the storage you already run.

Go SDK guide →
$ rdq dlq list --queue payments.charge
ID          ERROR             ATTEMPTS  AGE
01J8Z…K3    TimeoutException  5         2h14m
01J8Z…M7    TimeoutException  5         1h58m

# read one task's full failure history
$ rdq dlq get 01J8Z…K3

# fix shipped — redrive everything that timed out
$ rdq dlq redrive --queue payments.charge \
      --error TimeoutException --since 14:00
redriven 128 tasks  (audit id 01J90…)

A DLQ you can actually work with

Every attempt's error, stack, and timestamp lands in the DLQ. Browse and filter by queue, error type, handler, or time — then single- or bulk-redrive after a fix ships. Every mutation is audit-logged.

DLQ & redrive guide →

Everything rdq does

Retry policies & backoffThe outcome contractDLQ browse & redriveAtomic claims & leasesStorage SPI + compliance kitLanguage-neutral envelopeHandler registry & versionsREST & gRPC intakeHTTP / gRPC callbacksPrometheus metricsAudit log on every mutationrdq CLI

Quickstart

Run the central retry hub — any language talks to it over REST or gRPC.

# 1. Point rdq at storage you already run, and start the hub
docker run -e RDQ_DSN=postgres://user:pass@host/db \
           -p 8080:8080 ghcr.io/srjn45/rdq-server:latest

# 2. Submit a failed unit of work
curl -X POST localhost:8080/v1/queues/payments.charge/tasks \
  -d '{"handler_ref":"charge-payment","payload":"eyJvcmRlciI6NDJ9"}'

# 3. Later: inspect what never succeeded, then redrive after the fix ships
rdq dlq list --queue payments.charge
rdq dlq redrive --queue payments.charge --error TimeoutException

Full quickstart — Go, Java & server →

From the blog

Read the blog →