--- title: "DLQ Redrive & Replay-Bypass" description: "Operator tooling to move dead-lettered messages back onto a queue (ADR-0026) — Redrive resets the dead_letter block and attempts to 0 while preserving job, trace_id, data and meta verbatim, with dry-run, sandbox-routing and select filters — plus the Replay-Bypass guard (ADR-0027) that lets a deliberate replay skip external side-effects that already fired, via the out-of-band bq-replay-bypass transport header. The wire envelope stays frozen at schema_version 1." source: https://babelqueue.com/docs/spec/1.x/redrive-and-replay/ updated: 2026-06-21T00:00:00.000Z --- # DLQ Redrive & Replay-Bypass A message that exhausts its retries lands on a **dead-letter queue** carrying the optional [`dead_letter` block](/docs/spec/1.x/envelope/#optional-dead_letter-block). The [wire contract](/docs/spec/1.x/dead-letter-and-tracing/) defines how a message *gets* there but deliberately leaves getting it **back** to tooling. This page is that tooling: the cross-SDK **Redrive** operation (ADR-0026) and the **Replay-Bypass** guard (ADR-0027) that makes a replay safe to run. > **Status:** Authoritative · Operator tooling · envelope frozen at `schema_version: 1` Both are **tooling layers**. Redrive reads and re-publishes the existing frozen envelope; Replay-Bypass rides an **out-of-band transport header**, never a new envelope field. The wire stays at `schema_version: 1`. ## Redrive: move messages off the DLQ, reset `Redrive` drains dead-lettered messages from a DLQ and re-publishes each, **reset for reprocessing**: - the `dead_letter` block is **removed** and top-level `attempts` reset to **`0`**; - `job`, `trace_id`, `data` and `meta` are preserved **verbatim** — a redriven message is indistinguishable from a fresh one, and its `trace_id` keeps it on the original trace. By default each message goes back to its own `dead_letter.original_queue`; `ToQueue` overrides that to a **sandbox** queue so you can replay safely. Messages are drained from the DLQ first and then processed, so restored messages are never re-encountered in the same run; a DLQ message is acked only after its re-publish **succeeds**, and an undecodable ("poison") body is **restored, not lost**. The options are the safe-replay primitives: | Option | Effect | | :--- | :--- | | `DryRun` | Inspect and report the plan; every message is restored unchanged, nothing is re-published. | | `Select` | A predicate picking which messages to redrive (e.g. by `reason` or URN); unselected are restored. | | `ToQueue` | Re-publish to this queue instead of each message's source — route to a sandbox to replay safely. | | `Max` | Cap how many messages are pulled from the DLQ (`0` = all currently available). | | `Bypass` | Stamp the replay-bypass header on each redriven message (see below). | The run returns a result summarizing what happened — counts of redriven vs skipped, and a per-message item recording its `meta.id`, `trace_id`, URN, `dead_letter.reason`, target queue, and whether it was actually redriven. ## Replay-Bypass: don't re-fire effects that already ran Redrive solves *getting a message back*; it does not solve *what its handler does the second time*. A deliberate replay re-runs the handler — and its external side-effects re-fire: a second charge, a duplicate email. [Idempotency](/docs/spec/1.x/idempotency/) stops an **accidental** duplicate (same `meta.id` delivered twice); **Replay-Bypass** stops an **intended** reprocess from re-firing effects that already happened. They are complementary. The mechanism (ADR-0027): when `Redrive` runs with `Bypass`, it stamps the **`bq-replay-bypass`** header on each redriven message. This is an **out-of-band transport header**, not an envelope field — so the wire stays frozen, and it only propagates over a broker whose transport implements the optional header capability. It rides the **same seam** as the [`traceparent` propagation](/docs/spec/1.x/observability/#w3c-traceparent--true-cross-hop-span-linkage): the in-memory transport carries it, header-capable bindings carry it, and a binding that does not yet carry headers makes `Bypass` a no-op there. The runtime surfaces the marker to the handler, which wraps its **external, non-idempotent** side so a replay re-runs the idempotent core but skips the effects that already fired: - `IsReplay(ctx)` — is the message currently being handled a deliberate replay? - `BypassExternalEffects(ctx, fn)` — run `fn` **unless** this is a replay, in which case skip it. ## Go `Redrive(ctx, transport, dlq, RedriveOptions{...})` and the replay guard (`HeaderReplayBypass`, `IsReplay`, `BypassExternalEffects`) are in `babelqueue-go`: ```go import babelqueue "github.com/babelqueue/babelqueue-go" // dry run: report the plan, change nothing plan, err := babelqueue.Redrive(ctx, transport, "orders.dlq", babelqueue.RedriveOptions{DryRun: true}) // redrive to a sandbox, stamping bq-replay-bypass, only messages that failed on timeout res, err := babelqueue.Redrive(ctx, transport, "orders.dlq", babelqueue.RedriveOptions{ ToQueue: "orders.sandbox", Bypass: true, Select: func(env babelqueue.Envelope) bool { return env.DeadLetter != nil && env.DeadLetter.Reason == "failed" }, }) ``` The handler guards its external effects so a replay is safe: ```go app.Handle("urn:babel:orders:created", func(ctx context.Context, env babelqueue.Envelope) error { saveOrder(env) // idempotent core — always runs return babelqueue.BypassExternalEffects(ctx, func() error { return sendConfirmationEmail(env) // external effect — skipped on replay }) }) ``` ## Python `babelqueue.redrive.redrive(transport, dlq, ...)` mirrors the Go API; the guard is `is_replay` / `bypass_external_effects`: ```python from babelqueue import BabelQueue, is_replay, bypass_external_effects from babelqueue.redrive import redrive app = BabelQueue("redis://localhost:6379/0") # inspect, change nothing plan = redrive(app.transport, "orders.dlq", dry_run=True) # redrive to a sandbox, stamping bq-replay-bypass res = redrive(app.transport, "orders.dlq", to_queue="sandbox", bypass=True) ``` ```python @app.handler("urn:babel:orders:created") def on_order_created(data, meta): save_order(data) # idempotent core — always runs bypass_external_effects(lambda: send_email(data)) # external effect — skipped on replay ``` ## PHP PHP's `Transport` is publish-only, so `Redrive::run($io, $dlq, $options)` drives a `RedriveIO` (reserve / ack / publish) that you bind to your broker. The same reset contract holds — `dead_letter` removed, `attempts` to `0`, everything else preserved: ```php use BabelQueue\Redrive\Redrive; use BabelQueue\Redrive\RedriveOptions; $result = Redrive::run($io, 'orders.dlq', new RedriveOptions( toQueue: 'orders.sandbox', // safe sandbox replay dryRun: false, bypass: true, // stamp bq-replay-bypass (needs a HeaderRedriveIO) select: fn (array $env) => ($env['dead_letter']['reason'] ?? '') === 'failed', )); ``` `Redrive::reset($envelope)` is exposed on its own for callers that re-publish through their own machinery. The replay-bypass guard reads the marker off the delivered message (a `HasHeaders`), so the handler skips effects that already fired — the PHP mirror of Go's `BypassExternalEffects`: ```php use BabelQueue\Redrive\ReplayBypass; $handler = ReplayBypass::wrap(function (\BabelQueue\Contracts\ConsumedMessage $m): void { saveOrder($m); // idempotent core — always runs ReplayBypass::bypassExternalEffects($m, fn () => sendEmail($m)); // skipped on replay }); ``` ## Per-SDK Redrive ships across the SDKs with the same reset contract and safe-replay options, and the **Replay-Bypass core guard is now in all six** — the `bq-replay-bypass` marker is identical across SDKs, so it is cross-SDK. The behaviour is the same everywhere; only the surfacing differs by language: Go/Python carry the replay flag on the handler's context, Java on a `ThreadLocal` scope, and Node/.NET/PHP pass the delivered headers explicitly (PHP's core `Transport` is publish-only, so its guard is a header-reading helper like `Idempotent::wrap`, not a context-based one). | SDK | Redrive | Replay-Bypass guard | | :--- | :--- | :--- | | Go | `Redrive(ctx, t, dlq, RedriveOptions{…})` | `IsReplay` / `BypassExternalEffects` | | Python | `redrive(transport, dlq, …)` | `is_replay` / `bypass_external_effects` | | Java | `Redrive.redrive(t, dlq, opts)` | `Replay.isReplay` / `Replay.bypassExternalEffects` (`Replay.process`) | | Node | `resetForRedrive` + `RedriveIO.publishWithHeaders` | `isReplay(headers)` / `bypassExternalEffects(headers, fn)` | | .NET | `Redrive.RedriveAsync(t, dlq, opts)` (`IHeaderPublisher`) | `Replay.IsReplay(headers)` / `Replay.BypassExternalEffectsAsync(headers, fn)` | | PHP | `Redrive::run($io, $dlq, $options)` (`bypass: true`) | `ReplayBypass::isReplay` / `bypassExternalEffects` / `wrap` | The one honest limit is the **transport** layer, not the SDK: the marker only propagates over a broker whose transport carries out-of-band headers (the in-memory transport does; `HeaderPublisher`-capable bindings carry it, others fall back). Until a given broker binding wires the header, `Bypass` is a best-effort no-op on it and sandbox `ToQueue` routing remains the fallback — exactly the per-transport rollout shape the [`traceparent` propagation](/docs/spec/1.x/observability/#w3c-traceparent--true-cross-hop-span-linkage) follows, since both ride the same header seam. For a runnable walkthrough, see the [DLQ redrive example](/docs/examples/1.x/cross-language-redis/), and for the related de-duplication primitive, [Idempotency](/docs/spec/1.x/idempotency/). See each SDK's reference: [Go](/docs/babelqueue-go/1.x/introduction/), [Python](/docs/babelqueue-python/1.x/introduction/), [PHP](/docs/php-sdk/1.x/introduction/), [Java](/docs/babelqueue-java/1.x/introduction/), [Node](/docs/babelqueue-node/1.x/introduction/), [.NET](/docs/babelqueue-dotnet/1.x/introduction/).