An MQTT topic browser shows every topic flowing through a broker as a live, expandable tree, with the latest payload next to each node. In this tutorial you will generate a realistic topic hierarchy, explore it in the ConnectMQ live topic explorer and on the command line, and learn the patterns that reveal naming mistakes, stale retained messages and malformed payloads in seconds.
What an MQTT Topic Browser Does
MQTT has no built-in "list topics" command. Topics are not created or registered; they exist only because someone publishes to them. A topic browser works around this by subscribing to a broad wildcard such as # (or a project root like factory1/#) and building a tree from the topic strings it receives. Each slash becomes a level, each level becomes a folder, and the most recent payload is shown on the leaf.
That simple idea is surprisingly powerful. A flat stream of messages from mosquitto_sub is hard to read once you have more than a dozen devices. A tree tells you at a glance:
- Which branches exist, and which ones should not.
- Which leaves are updating right now, and which stopped hours ago.
- Which values are retained, meaning a new subscriber will receive them immediately.
- How large and how well-formed the payloads are.
If topic structure is new to you, it helps to understand what an MQTT broker does with topics and subscriptions before you start.
Generate a Topic Tree to Explore
You need traffic to browse. The script below publishes a small factory hierarchy to the free public MQTT broker at broker.connectmq.com. It deliberately includes three common mistakes so you can find them in the browser. Install the client with pip install "paho-mqtt>=2.0".
import json
import random
import time
import uuid
import paho.mqtt.client as mqtt
RUN_ID = uuid.uuid4().hex[:6]
ROOT = f"connectmq/demo/{RUN_ID}"
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=f"tree-gen-{RUN_ID}",
)
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_start()
print(f"Publishing under {ROOT}/ (subscribe to {ROOT}/#)")
# Retained device metadata
for line in ("line1", "line2"):
for machine in ("cnc1", "cnc2"):
meta = {"vendor": "demo", "model": "X200", "line": line}
client.publish(f"{ROOT}/factory1/{line}/{machine}/meta",
json.dumps(meta), qos=1, retain=True)
# Mistake 1: inconsistent casing creates a parallel branch
client.publish(f"{ROOT}/factory1/Line1/cnc1/meta", "{}", qos=1, retain=True)
# Mistake 2: trailing slash creates an empty last level
client.publish(f"{ROOT}/factory1/line2/cnc2/telemetry/", "42", qos=0)
try:
while True:
for line in ("line1", "line2"):
for machine in ("cnc1", "cnc2"):
payload = {
"spindle_rpm": random.randint(9000, 12000),
"temperature": round(random.uniform(35, 60), 1),
"ts": int(time.time()),
}
client.publish(f"{ROOT}/factory1/{line}/{machine}/telemetry",
json.dumps(payload), qos=0)
# Mistake 3: a payload that is not valid JSON
client.publish(f"{ROOT}/factory1/line1/cnc2/telemetry",
"temp=48.2;rpm=10100", qos=0)
time.sleep(2)
except KeyboardInterrupt:
client.loop_stop()
client.disconnect()
The script publishes four messages per cycle plus the malformed one, which keeps it comfortably inside the public broker's limit of 10 messages per second per client. Leave it running and note the printed root.
Explore the Tree in the ConnectMQ Topic Explorer
On your own broker, the live topic explorer in the ConnectMQ portal is the built-in topic browser. To follow along, create a free broker at the portal, then point the script above at it by switching to port 8883 with TLS and adding the credentials the portal generated (YOUR_USERNAME, YOUR_PASSWORD, YOUR_CLIENT_ID). For a quick look at the public broker instead, any MQTT explorer that supports WebSockets can connect to wss://broker.connectmq.com:8084/mqtt and subscribe to your connectmq/demo/<run-id>/# root.
As messages arrive, walk the tree from the top:
- Expand the root. You should see
factory1and, beneath it,line1,line2and an unexpectedLine1. Topics are case-sensitive, soLine1is a separate branch that no subscriber toline1/#will ever receive. - Open
line2/cnc2. Undertelemetrythere is a child with an empty name. That is the trailing slash:telemetry/has one more level thantelemetry, and a subscription to.../cnc2/telemetrywill not match it. - Open
line1/cnc2/telemetry. The value flips between JSON andtemp=48.2;rpm=10100. Any consumer that parses JSON will throw on every second message. - Look at the
metaleaves. They carry the retained flag. Disconnect and reconnect, and they appear immediately, while telemetry only shows up when the next message arrives.
The ConnectMQ portal also offers a devices view organised by ISA-95 levels (enterprise, site, area, line, cell). If your topics follow a Unified Namespace layout, that view groups the same data by plant hierarchy instead of raw strings, which makes misplaced devices stand out. Our article on Sparkplug B and the Unified Namespace explains that structure.
Browse Topics from the Command Line
A graphical browser is not always at hand. mosquitto_sub with the -v flag prints the topic before each payload, and a few extra flags turn it into a lightweight browser:
# Every topic under your root, with topic names
mosquitto_sub -h broker.connectmq.com -p 1883 -i browse-$RANDOM \
-t 'connectmq/demo/RUN_ID/#' -v
# Only retained messages: connect, print what is retained, exit after 3 s
mosquitto_sub -h broker.connectmq.com -p 1883 -i retained-$RANDOM \
-t 'connectmq/demo/RUN_ID/#' -v --retained-only -W 3
# Unique topic names seen in 30 seconds
timeout 30 mosquitto_sub -h broker.connectmq.com -p 1883 -i list-$RANDOM \
-t 'connectmq/demo/RUN_ID/#' -F '%t' | sort -u
Replace RUN_ID with the value the script printed. The last command is the closest thing MQTT has to "list topics", and it is a quick way to diff the topics you expect against the topics that actually exist. More recipes are in our mosquitto_pub and mosquitto_sub examples.
Debugging Patterns a Topic Browser Reveals
Once you are used to reading a tree, most topic problems fall into a small number of patterns.
| What you see in the tree | What it means | Fix |
|---|---|---|
| Two branches that differ only by case | Inconsistent naming in firmware or config | Standardise on lowercase and enforce it in code review |
| A leaf with an empty name | Trailing slash in the publish topic | Strip trailing slashes before publishing |
| A first level with an empty name | Leading slash (/factory1/...) |
Remove the leading slash; it adds a useless level |
| A leaf whose value never changes | Device stopped publishing, or the value is stale retained data | Check the device; clear the retained message if obsolete |
| Values alternating between formats | Two firmware versions or two publishers on one topic | Version the payload or separate the topics |
| Hundreds of sibling leaves with IDs in the name | Unique IDs placed too high in the hierarchy | Move IDs down to the device level |
| Very deep branches | Over-structured topics | Flatten; the public broker allows 8 levels |
Clearing Stale Retained Messages
Retained values persist on the broker until replaced. A decommissioned device can therefore keep showing a "current" value in every dashboard. To remove a retained message, publish an empty retained payload to the exact topic:
mosquitto_pub -h broker.connectmq.com -p 1883 -i clear-$RANDOM \
-t 'connectmq/demo/RUN_ID/factory1/Line1/cnc1/meta' -r -n
The -n flag sends a zero-length payload and -r sets the retain flag. Refresh the topic browser and the Line1 branch disappears once it has no other children.
Using a Topic Browser Well in Production
A browser that subscribes to # on a busy production broker receives every message, which costs bandwidth and can hit subscription or connection quotas. A few habits keep it useful:
- Subscribe to the narrowest root that answers your question, such as
factory1/line2/#, rather than#. - Use a dedicated credential for debugging tools, so a browser session never takes a device's client ID and knocks the device offline.
- Check connection state as well as data. The topic
$SYS/broker/connection/{clientId}/stateon your ConnectMQ broker tells you whether a specific client is connected, which separates "device offline" from "device online but silent". - Write down your naming rules and compare the tree against them after every firmware release.
For editor-centric workflows, the ConnectMQ VS Code extension and the ConnectMQ Desktop MQTT explorer offer the same tree-style browsing outside the portal. If you are testing from a browser only, our guide to using an MQTT web client online pairs well with this one, and MQTT broker monitoring covers the metrics a topic browser does not show.
Explore Your Own Topics on ConnectMQ
Create a free broker at app.connectmq.com on the Serverless plan, with no credit card required. The portal includes the live topic explorer, the in-browser client and the ISA-95 devices view used in this guide. Learn more about ConnectMQ as a fully managed MQTT service, or read the broader guide to choosing a cloud MQTT broker.
Frequently Asked Questions
Can I list all topics on an MQTT broker?
Not directly; MQTT has no topic registry. A topic browser subscribes to a wildcard and shows the topics that receive messages, plus any retained topics, which is the practical equivalent.
Why does my topic browser not show a topic I know exists?
Either nothing has been published to it since you subscribed and it holds no retained message, or your wildcard does not cover it. Check case, leading or trailing slashes, and the subscription root.
Is subscribing to # safe on a production broker?
It works but receives every message, which can be heavy. Subscribe to the narrowest root that answers your question and use a dedicated debugging credential.
How do I delete a topic from the tree?
Topics disappear when nothing holds them. Clear a retained message by publishing an empty retained payload to that exact topic, and stop the publisher that creates it.



