The operations console for RabbitMQ

Your dead letters, handled before the 3 a.m. call.

The management UI tells you a queue holds 300 messages. Warren tells you what they are, why they died, which failure is new, and whether your replay actually worked.

Self-hosted next to your broker. RabbitMQ 3.13 and 4.x, classic and quorum queues.

Early access: every Team and Pro feature free until 31 March 2027 →

Free, no signupOne container, your data stays in your networkListed in the RabbitMQ developer tools

Warren · production · vhost /
Failures14 dead lettersacross 3 dead-letter queues
FailureMessagesIn queues
expired in orders.wait.5mnew3orders.dlq 3
PaymentDeclinedExceptionnote6orders.dlq 4 · +1
OrderNotFoundException5orders.dlq 2 · +1

The same failure in every dead-letter queue is one row. New ones are marked, and can page you.

expired in orders.wait.5mnew3 in orders.dlq
Message idLast deathOriginal routePayload
order-4700expired ×5 wait.5morders → order.created{"orderId":4700,"customer":"ACME…
order-4701expired ×5 wait.5morders → order.created{"orderId":4701,"customer":"ACME…
order-4702expired ×5 wait.5morders → order.created{"orderId":4702,"customer":"ACME…
Replay messages
All 3 messages of this failure
First 10 messages from the head of the queue
Original route from x-death
Queue · Exchange
Back to their original route, death headers stripped. Warren then watches whether they come back.
ReplaysAudit log
WhenFrom → ToReplayed
just now · ninaorders.dlq → original3 1 back
Tue · ops-botbilling.invoice.dead → original12
Mon · tomasorders.dlq → order.retry0 failed

Every replay: who, when, from where to where, per-message outcome, payload preview.

Replay completed3 replayed, 0 failed, 3 scanned
A new failure turns up across your dead-letter queues
Only in Warren

What the management UI, a shovel and a script cannot tell you.

Keep the management UI for configuring the broker. Warren runs the traffic on it, and never touches queues, exchanges or policies. Compared in detail: Warren vs. the management UI and shovel, scripts and other tools.

One failure, every queue. And the new one marked.

The same exception in five dead-letter queues is one row with each queue's count, and one replay sends them all back to their original routes. A failure that was not there before is tagged new, and an alert can page you the moment it appears, not when the usual timeouts pile up a little more.

Failures across queues →
The Failures page: six failures across the dead-letter queues of a cluster, one tagged new, one with a team note

Did the replay work? Warren checks.

A confirmed publish only says the broker took the message. Warren finds replayed messages that die again and counts them against their replay: "1 of 2 died again, back in payments.refund.dlq", marked per message. It looks again after 5 minutes, 30 minutes, 2 hours and a day.

Replay outcome →
A replay's page: one of two replayed refunds died again and is marked back in DLQ

Exactly the messages you saw, and an undo.

"The first 500" or "every match" is checked first: Warren freezes those messages, shows how many, and the confirm takes exactly them. Discarded and purged messages are kept for seven days, so one click on Restore puts them back. In Pro a second operator can approve first.

Dry runs and undo →
Dry run: exactly these 20 messages of 171 will be discarded, waiting for a second operator
Watch it

A new failure, replayed, and the one that came back. Twenty seconds.

Recorded in the real app. Pick a recording below; the big one plays it.

A tax service outage shows up on the Failures page as a new failure. One replay sends its five messages back to the orders route; one dies again, and the audit log says so: 1 of 5 back in orders.dlq.

The whole story in four minutes: a step-by-step walkthrough on YouTube, from 200 dead letters in the management UI to a throttled replay, with a note for the team, parking, the audit log and replay rules.

Features

Built for the moment a queue turns red.

Browsing and replaying are in the recordings above. These keep a team out of the DLQ in the first place.

History and alerts

Know about the pile-up before the customer does.

Warren samples every queue around the clock. A rule like “orders.dlq above 100 for five minutes” fires once, reaches Slack, Teams, PagerDuty, Opsgenie, e-mail or any webhook with a link to the queue, and sends the all-clear when it recovers; in PagerDuty and Opsgenie that closes the incident. No flood, no silence. A rule on the inflow rate catches a mass failure even while a replay rule keeps draining the queue.

MESSAGES_ABOVE · NO_CONSUMERS · GROWTH · INFLOW_RATE · disk · memory · re-notify after 4 h
orders.dlq · production · history 1 h
RuleConditionChannelState
Orders DLQ growingmessages > 100 for 300 s#ops-alertsOK
Billing stuckno consumers, ready > 0#billingOK
Warren APP · 11:07
🔴 FIRING: Orders DLQ growing on production/orders.dlq: 143 messages (threshold 100, for 300s) · open queue
Sampling every 30 seconds
  • Replay rulesThe 3 a.m. timeout replays itself on a schedule, with backoff and attempt limits; the broken message waits for you.
  • Payloads you can readJSON as a tree or a table; PDFs, images and CSV inside payloads open as files.
  • Edit before replayFix a field or drop the retry counter, marked as edited in the audit log.
  • Park poison messagesOut of the way of replays and rules, with their death history intact.
  • Spring and MassTransitException headers, _error and _skipped queues, replays back to the endpoint.
  • Set-up checksQueues that drop dead letters, exchanges into nowhere, and a map of where they go.
  • Personal data hiddenTokens, IBANs and e-mail addresses masked in the view; replays keep the real data.
  • Roles and SSOSupport looks, on-call acts. OIDC sign-in in Pro.
  • Audit logSearchable, shareable as a link; CSV and SIEM forwarding in Pro.
  • Keyboard all the wayj k x r, ⌘K for any action, f puts a letter on every button.
  • PrometheusQueue, broker and alert metrics with ready-made alert rules, in Team and Pro.
  • Export, import, publishNDJSON out before a purge, back in later or on staging, or one test message.
How replay works

No message is ever lost. Here is the exact mechanism.

  1. Own channel, confirms onNothing is shared with your consumers.
  2. Fetch, do not acknowledgeUnselected messages stay where they were.
  3. Publish, confirm, then ackThe source is acknowledged only after the broker confirmed the copy.
  4. Release the restA crash mid-way releases everything not yet moved.

At-least-once, like RabbitMQ itself: if Warren dies between the confirm and its own ack, that one message arrives twice. On RabbitMQ 3.13, where every peek of a quorum queue counts as a delivery, Warren asks before it reads. The details →

Self-hosted

Runs next to your broker. Your payloads never leave your network.

One container with an embedded database, or point Pro at your own PostgreSQL. Configure clusters with environment variables or a YAML file, put it behind your ingress, sign in with SSO. Air-gapped installations work: the licence is checked offline.

  • Docker image, versioned releases with a changelog, database migrations run on upgrade
  • Reads through the management API, replays over AMQP with a least-privilege user
  • Metrics and audit stay in your network, embedded or in your Postgres, retention is yours to set; Pro forwards the audit log to your SIEM
  • No telemetry, no phone-home, no third-party scripts

A RabbitMQ, Warren and three dead-letter queues with real dead letters. Only Docker needed.

  1. 1
    docker-compose.try.yml

    Copy it and save it as docker-compose.try.yml. Read it first if you like: a RabbitMQ, a one-off step that fills three queues with dead letters, and Warren.

    # Try Warren in a minute: a RabbitMQ, Warren next to it, and dead letters to look at.
    #
    #   docker compose -f docker-compose.try.yml up -d      →  http://localhost:8080, sign in as admin / admin
    #   docker compose -f docker-compose.try.yml down -v    →  everything gone again
    #
    # The seed step fills a few queues the way real consumers would: orders rejected by the consumer,
    # invoices that expired while nobody listened, shipping labels a Spring consumer republished with its
    # exception. Open orders.dlq, look at why they died, select a few and replay them. Nothing here
    # touches a broker of your own; the RabbitMQ management UI is on http://localhost:15680 (warren / warren), off the
    # default port so it never collides with a RabbitMQ you already run.
    # Ports taken? TRY_WARREN_PORT=8081 TRY_RABBIT_UI_PORT=15690 docker compose -f docker-compose.try.yml up -d
    name: warren-try
    
    services:
      rabbitmq:
        image: rabbitmq:4-management
        environment:
          # guest may only sign in from localhost; Warren and the seed step come from their own containers
          RABBITMQ_DEFAULT_USER: warren
          RABBITMQ_DEFAULT_PASS: warren
        ports:
          - "${TRY_RABBIT_UI_PORT:-15680}:15672"
        healthcheck:
          test: ["CMD", "rabbitmq-diagnostics", "-q", "check_port_connectivity"]
          interval: 5s
          retries: 30
    
      # Declares the demo topology and dead-letters messages through the management API, then exits.
      # Runs once: when orders.dlq already holds messages it leaves them alone.
      seed:
        image: curlimages/curl:8.11.1
        depends_on:
          rabbitmq: { condition: service_healthy }
        restart: "no"
        entrypoint: ["/bin/sh", "-c"]
        command:
          - |
            set -e
            api() { curl -sf -u warren:warren -H 'content-type: application/json' -X "$$1" "http://rabbitmq:15672/api$$2" $${3:+-d "$$3"} > /dev/null; }
            if curl -sf -u warren:warren http://rabbitmq:15672/api/queues/%2F/orders.dlq | grep -q '"messages":[1-9]'; then echo "already seeded"; exit 0; fi
    
            # orders: the consumer rejects, the broker dead-letters to orders.dlq
            api PUT /exchanges/%2F/orders '{"type":"topic","durable":true}'
            api PUT /exchanges/%2F/orders.dlx '{"type":"fanout","durable":true}'
            api PUT /queues/%2F/orders.dlq '{"durable":true}'
            api PUT /queues/%2F/orders.process '{"durable":true,"arguments":{"x-dead-letter-exchange":"orders.dlx"}}'
            api POST /bindings/%2F/e/orders.dlx/q/orders.dlq '{"routing_key":""}'
            api POST /bindings/%2F/e/orders/q/orders.process '{"routing_key":"order.*"}'
            # billing: invoices with a TTL, nobody consumes, they expire into billing.invoice.dead
            api PUT /exchanges/%2F/billing '{"type":"direct","durable":true}'
            api PUT /exchanges/%2F/billing.dlx '{"type":"fanout","durable":true}'
            api PUT /queues/%2F/billing.invoice.dead '{"durable":true}'
            api PUT /queues/%2F/billing.invoice '{"durable":true,"arguments":{"x-message-ttl":2000,"x-dead-letter-exchange":"billing.dlx"}}'
            api POST /bindings/%2F/e/billing.dlx/q/billing.invoice.dead '{"routing_key":""}'
            api POST /bindings/%2F/e/billing/q/billing.invoice '{"routing_key":"invoice"}'
            # shipping: a Spring consumer gave up and republished to the DLQ itself (RepublishMessageRecoverer)
            api PUT /exchanges/%2F/shipping '{"type":"direct","durable":true}'
            api PUT /queues/%2F/shipping.label '{"durable":true}'
            api PUT /queues/%2F/shipping.label.dlq '{"durable":true}'
            api POST /bindings/%2F/e/shipping/q/shipping.label '{"routing_key":"label"}'
    
            customers="ACME Northwind Contoso Initech Globex"
            i=0
            for c in $$customers $$customers $$customers $$customers; do
              i=$$((i + 1)); id=$$((4700 + i))
              if [ $$((i % 4)) -eq 0 ]; then err='Customer '$$c' is locked'; type=com.acme.orders.CustomerLockedException
              else err='Timeout calling inventory after 5000 ms'; type=java.net.SocketTimeoutException; fi
              api POST /exchanges/%2F/orders/publish "{\"routing_key\":\"order.created\",\"payload_encoding\":\"string\",\"payload\":\"{\\\"orderId\\\":$$id,\\\"customer\\\":\\\"$$c\\\",\\\"total\\\":$$((i * 37)).50}\",\"properties\":{\"message_id\":\"order-$$id\",\"content_type\":\"application/json\",\"headers\":{\"x-exception-type\":\"$$type\",\"x-exception-message\":\"$$err\"}}}"
            done
            api POST /queues/%2F/orders.process/get '{"count":20,"ackmode":"reject_requeue_false","encoding":"auto"}'
            # three orders still on their way, so orders.process is not empty
            for id in 4801 4802 4803; do
              api POST /exchanges/%2F/orders/publish "{\"routing_key\":\"order.updated\",\"payload_encoding\":\"string\",\"payload\":\"{\\\"orderId\\\":$$id}\",\"properties\":{\"message_id\":\"order-$$id\",\"content_type\":\"application/json\"}}"
            done
    
            for n in 1 2 3 4 5 6; do
              api POST /exchanges/%2F/billing/publish "{\"routing_key\":\"invoice\",\"payload_encoding\":\"string\",\"payload\":\"{\\\"invoiceNo\\\":\\\"2026-10$$n\\\",\\\"amount\\\":$$((n * 120))}\",\"properties\":{\"message_id\":\"inv-2026-10$$n\",\"content_type\":\"application/json\"}}"
            done
    
            for n in 1 2 3 4; do
              api POST /exchanges/%2F/amq.default/publish "{\"routing_key\":\"shipping.label.dlq\",\"payload_encoding\":\"string\",\"payload\":\"{\\\"shipmentId\\\":\\\"SH-90$$n\\\",\\\"carrier\\\":\\\"DHL\\\"}\",\"properties\":{\"message_id\":\"label-90$$n\",\"content_type\":\"application/json\",\"headers\":{\"x-original-exchange\":\"shipping\",\"x-original-routing-key\":\"label\",\"x-exception-message\":\"Carrier API answered 503 Service Unavailable\",\"x-retry-count\":3}}}"
            done
            echo "seeded: 20 in orders.dlq, 6 expiring into billing.invoice.dead, 4 in shipping.label.dlq"
    
      warren:
        image: ghcr.io/viba88/warren:0.17
        depends_on:
          rabbitmq: { condition: service_healthy }
        ports:
          - "${TRY_WARREN_PORT:-8080}:8080"
        environment:
          WARREN_RABBIT_ID: try
          WARREN_RABBIT_NAME: Try-out broker
          WARREN_RABBIT_MANAGEMENT_URL: http://rabbitmq:15672
          WARREN_RABBIT_HOST: rabbitmq
          WARREN_RABBIT_USERNAME: warren
          WARREN_RABBIT_PASSWORD: warren
          WARREN_ADMIN_USERNAME: admin
          WARREN_ADMIN_PASSWORD: admin
        volumes:
          - warren-data:/app/data
    
    volumes:
      warren-data:
  2. 2
    docker compose -f docker-compose.try.yml up -d
  3. 3
    Open localhost:8080, sign in as admin / admin, open orders.dlq.

Done? docker compose -f docker-compose.try.yml down -v removes it all.

No Docker at hand? See what you would get: the recordings above, or the four-minute walkthrough on YouTube.

Early access

Every Team and Pro feature, free until 31 March 2027.

Single sign-on, roles, four-eyes approval, audit export to your SIEM, alerts, replay rules, 90 days of history and your own PostgreSQL. Ask for a key and it comes by e-mail. Afterwards Warren falls back to Community, nothing is deleted, and early-access users get 50% off their first licence.

Request a free key
FAQ

Questions operators ask first.

Is Warren a replacement for the management UI?

No, and it is not trying to be. The management UI configures the broker and will keep doing that. Warren is for the people who run the traffic on it: reading messages, finding out why they died, alerting, replaying, and proving afterwards what was done. Most teams have both open. What each one is for, side by side.

Does Warren change my broker configuration?

No, by design: a tool that can replay production messages should not also be able to reshape the broker. It reads queues, bindings and messages through the management API and publishes over AMQP; no policies, no exchanges, no bindings. It never deletes queues, exchanges or bindings; the only queue it creates is a parking queue, when an operator parks messages. The only destructive actions are discarding messages and purging a queue: operators only, with a reason, recorded in the audit log.

What permissions does the RabbitMQ user need?

The management tag plus read and write on the vhosts you configure, and the monitoring tag if Warren should show disk and memory of the nodes. Configure permission is only needed if Warren should create parking queues itself; without it, create <queue>.parking once and Warren uses it as it is.

Does peeking at messages have side effects?

RabbitMQ marks peeked messages as redelivered. That is a property of the broker and is visible to consumers as a flag; the messages stay in place and in order. On RabbitMQ 3.13 a peek of a quorum queue with a delivery limit also counts as a delivery; Warren asks before it reads such a queue.

Will support see customers' personal data?

Not by default. The message view hides values whose field name looks sensitive (password, token, IBAN, e-mail and more, configurable with WARREN_MASKING_FIELDS) and anything that looks like an e-mail address, an IBAN or a card number. Operators and admins can show one message's values, viewers cannot; in Community, where every user is an admin, anyone signed in can. This is display masking: replays, downloads and exports keep the real data, and so does the API for anyone with access.

Which RabbitMQ versions are supported?

3.13 and 4.x, classic and quorum queues. Streams are read-only for now.

Team or Pro?

Team is for a team that runs its own brokers: up to three, with any number of vhosts each, roles so support can look while on-call acts, history, alerts, and rules that retry dead letters by themselves. Pro is for a company that has to show what happened: any number of brokers, four-eyes approval for replays, discards and purges, the audit log as CSV and in your SIEM, 90 days of history, sign-in through your identity provider, and your own PostgreSQL. A Team licence upgrades to Pro in place; nothing is reinstalled. A licence covers one installation: one running Warren, however many brokers and vhosts it watches, so prod, staging and dev on one Warren count once.

What does Team or Pro cost?

Nothing until 31 March 2027: Warren is in early access, and every Team and Pro feature is free with a key you can request here. After that date Warren falls back to Community until a new key is installed; nothing is deleted, and early-access users get 50% off their first licence. Community stays free.

Is Warren open source?

Not at the moment. Warren is distributed as a ready-to-run container image; the Community edition is free to use for any purpose, without registration or time limit. The source may open up later; if that matters to you, tell us. The compose file is part of the documentation; bugs and wishes go through the feedback form.

Where is my data?

In your network. Warren stores replay audit entries, queue metrics and users in its embedded database on a volume you own, or in your own PostgreSQL with Pro. The audit log keeps the first 16 KiB of each moved message by default; WARREN_AUDIT_PAYLOAD_BYTES sets how much.

Get started

Your first replay is three commands away.

Download the compose file, start it, open localhost:8080. A RabbitMQ with real dead letters is included, so there is nothing to prepare. Free, no account.