DEV Community

Cover image for One TUI for RabbitMQ, Kafka, and MQTT: why I built queuepeek
Matías Denda
Matías Denda

Posted on

One TUI for RabbitMQ, Kafka, and MQTT: why I built queuepeek

The problem I was trying to solve

I work across a few projects that all talk to message brokers, but never the same one. Some services are on RabbitMQ. The data pipeline runs on Kafka. A handful of IoT integrations use MQTT. Normal stuff.

What wasn't normal was the amount of context-switching every time something went wrong in production. Three different web UIs, three different mental models, three different ways to peek at a message without accidentally consuming it.

The RabbitMQ Management UI is fine, but it's a web app — and half the time I'm already in a terminal next to the logs. Kafka UIs are a whole can of worms (every company seems to use a different one). MQTT doesn't really have a good "just let me see what's retained on this topic" tool for free.

So after one too many incidents where I wanted to diff two DLQ messages and ended up pasting JSON into an online diff tool, I started building what became queuepeek.

What it is

queuepeek is a terminal UI written in Rust (on top of ratatui) that speaks RabbitMQ, Kafka, and MQTT from the same interface. You launch it, pick a profile, drill down through queues/topics, and land on individual messages.

The entire thing is keyboard-driven and follows the same wizard flow regardless of the broker:

Profiles -> Queues/Topics -> Messages -> Message Detail

Esc always pops one level up. / always filters. ? always shows help contextual to where you are and which broker you're on.

No mouse. No tabs. No switching apps.

Design choices that paid off

Non-destructive peek

This was the whole point. A "queue inspector" that consumes messages while you're reading them is not an inspector — it's a silent bug waiting to happen.

  • For RabbitMQ, queuepeek uses the Management HTTP API's get endpoint with ack_requeue_true. You read, the message stays.
  • For Kafka, every read session spins up an ephemeral consumer with a unique group ID. You're not stealing offsets from anyone.
  • For MQTT, you're subscribing to a topic so it's inherently non-mutating, but retained message management has its own explicit screen with a clear "this will clear the retained payload" confirmation.

Sounds basic. A surprising number of tools don't do this.

Same keybindings, different brokers

Kafka doesn't have queues, it has topics. RabbitMQ has exchanges and bindings. MQTT has topic hierarchies. Under the hood these are very different beasts, but from the user's point of view, "show me what's in here" should feel the same.

The footer at the bottom of every screen dynamically filters shortcuts to the backend you're connected to — G:groups only shows on Kafka, X:topology only on RabbitMQ, H:retained only on MQTT. No dead keys, no "this doesn't work on your broker" errors.

Multi-select bulk ops

Checkboxes in a TUI sound fiddly, but they turned out to be one of the most useful features. Space toggles selection on the current message, then any operation you trigger (delete, copy, move, export) applies to all selected messages — streamed, not loaded into memory.

Want to delete 10,000 messages from a DLQ after confirming none of them match a pattern you care about? Filter, select all, delete. Done from the keyboard.

A few features I'm particularly happy with

Side-by-side message diff. Select two messages, press d, get a colored diff. Uses the similar crate. Embarrassingly useful for "why does this one message fail and the other succeed?"

Schema Registry integration. If you configure a Confluent-compatible Schema Registry URL, queuepeek auto-decodes Avro and raw Protobuf payloads using the Confluent wire format (magic byte 0x00 + 4-byte schema ID + body). Toggle raw/decoded with
s.

Real concurrent benchmarking. Press F5 on a queue to run a flood-publish benchmark with N worker threads (via std::thread::scope), rendering a live gauge and p50/p95/p99 latency percentiles at the end. Useful for capacity-planning conversations
that usually start with "how fast can this queue take?"

Webhook alerts. Configure a regex pattern in config.toml, point it at a webhook URL, and queuepeek polls every 30 seconds and POSTs on match (deduplicated by message hash so you don't spam yourself). Good for catching a specific error pattern appearing in a queue before it becomes an incident.

Payload templates with interpolation. Ctrl+T to save the current message, Ctrl+W to insert it. Supports variables like {{timestamp}}, {{uuid}}, {{random_int}}, {{counter}}, and {{env.VAR}} for anything you export in your shell.

DLQ reroute. RabbitMQ x-death headers get parsed and displayed, and L re-routes a message back to its original exchange. Mostly because I got tired of manually copy-pasting routing keys out of x-death.

Interesting implementation bits

A few things that weren't obvious going in:

  • ratatui + crossterm + mpsc channels is all you need for a responsive TUI. Background I/O (broker calls, file operations, webhook polling) runs in std::thread workers and posts results back through a channel that the event loop drains each tick.
    No async runtime, no Tokio. The whole app feels instant.

  • Auto-refresh is dumb and works. Queue list refreshes every 5 seconds, message list every time tail mode is on. No WebSockets, no push. The Management API is cheap enough that polling is fine.

  • Scheduled messages persist to disk. ~/.config/queuepeek/scheduled.json with epoch seconds. If you schedule a publish and the app crashes or you close it, the schedule survives.

  • 79 unit tests across filters, comparison, operations, schema decoding, and config. TUI logic is hard to test end-to-end, but the pure functions underneath are easy.

Try it

# From crates.io (needs cmake for librdkafka)
cargo install queuepeek
Enter fullscreen mode Exit fullscreen mode

Or grab a prebuilt binary: https://github.com/matutetandil/queuepeek/releases

Supported platforms: macOS (ARM/Intel), Linux (x86), Windows (x86/ARM). Linux ARM builds need cargo install because cross-compiling librdkafka is its own adventure.

Minimal ~/.config/queuepeek/config.toml:

[profiles.local]
type     = "rabbitmq"
host     = "localhost"
port     = 15672
username = "guest"
password = "guest"
vhost    = "/"
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/matutetandil/queuepeek

Docs: /docs folder covers configuration, keyboard shortcuts, backends, and architecture.

What I'd love feedback on

It's a solo side project and I'm the main user. That means the keybindings reflect my own muscle memory, the defaults reflect my own workflows, and the rough edges are the ones I don't personally bump into.

If you spend time in any of these brokers and something feels off — a missing shortcut, a feature you'd expect, a decoding format I'm not handling — please open an issue. Especially on MQTT, which I use least.

MIT licensed. No telemetry. No signup. Just a binary that reads queues.

Top comments (0)