# Reading an unfamiliar MQTT broker in a building

> A subscriber cannot list topics, the retain flag is how you tell a stored value from a live one, and a duplicate client ID disconnects someone else.

Source: https://plantroomlabs.com/notes/reading-an-unfamiliar-mqtt-broker/  
Published: 2026-09-27 (27 September 2026) · Plantroom Labs  
Topics: MQTT, Integration, Commissioning

You have a hostname, a password and a claim about what is on it. MQTT gives you no directory, no timestamps and two ways to break the site while only reading.

## There is no list of topics, and there never will be

MQTT has no directory service. A broker does not publish what topics exist, because as far as the protocol is concerned a topic is not a thing that exists — it is a string in a message that happened to be sent. A client subscribed to `#` sees every message published from that moment on, and that is the whole of its visibility.

Which means the first thing to be clear about with anyone waiting on your answer: what you can produce is an observation over a window, not an inventory. A device that publishes every hour is invisible in a five-minute look, and a device that has been unplugged for a month is invisible for good — unless it left something retained, which is the next section and the most useful thing on this page.

One exception, and it is a trap. A subscription to `#` does **not** match topics beginning with `$`. Brokers put their own diagnostics there — client counts, message totals, uptime — and you have to ask for that tree by name to see any of it. Many deployments disable or restrict it, so its absence tells you nothing about the broker's health.

A second, smaller trap in the wildcard rules: a subscription to `site/#` matches `site` itself, whereas `site/+` does not. And a single `+` matches an empty level, so `a//b` — which is a legal topic with an empty middle level — is matched by `a/+/b`. Topic trees built by string concatenation produce empty levels more often than anyone intends.

## Retained messages, and the dead sensor they hide

A publisher can mark a message retained, and the broker then keeps exactly that one message per topic and hands it to every new subscriber the moment it subscribes. It is how a dashboard shows a value before the next reading arrives, and it is sensible design.

It is also how a value outlives the thing that measured it. A gateway retains a reading, the gateway is decommissioned, and the last number it ever sent is served to every new subscriber for as long as the broker lives. Every dashboard upstream shows it as current, because nothing in the message says otherwise.

Here is the part that makes it findable. **The retain flag on a message as it is delivered is meaningful, and it is not the same flag the publisher set.** When the broker sends a stored message to a client that has just subscribed, that flag is set. When it forwards a live message to a client that was already subscribed, the flag is cleared — even if the publisher marked it retained. So a client can tell, per message, "this is the broker's stored copy" from "this just happened". A topic that arrives with the flag set in the first instant and never speaks again is a candidate corpse, and one that keeps arriving with the flag clear is alive.

Two footnotes for anyone relying on that. Under MQTT 5 a subscription can ask for *retain as published*, which preserves the publisher's flag instead — useful for bridges, and it destroys exactly the distinction above, so a diagnostic client should not set it. And an empty payload published with the retain flag is not a broken message: it is the defined way to *delete* a retained value. A zero-byte payload in a tap is a tombstone, and seeing several usually means somebody has been cleaning up.

## Nothing in the protocol tells you how old a value is

There is no timestamp in an MQTT message. Not in the fixed header, not in the variable header, nowhere. Any time you have ever seen against an MQTT value came from inside the payload, where the integrator chose to put it, or from the clock on the machine that received it.

MQTT 5 adds one genuine piece of age information, and it is worth knowing because it is the only one: a publisher can set a message expiry interval, and when the broker forwards a message it has been holding it must reduce that interval by however long it held it. So on a v5 broker a retained message can arrive announcing how much of its life is left, which is a lower bound on its age. Nobody in building automation sets it, but when it is there it is the only honest age in the system.

Everything else is inference: no update during the window, plus arriving retained. That is evidence. Confirm it against the device before telling anybody their sensor is dead, because the alternative explanation — a publisher on a long interval — looks identical.

## The two ways a read-only client can still break something

**A duplicate client identifier disconnects the other client.** Client IDs must be unique on a broker, and the specified behaviour when a second connection arrives using an ID that is already in use is that the broker drops the existing one. Connect a diagnostic tool as `gateway1` and you have just taken a gateway off the air, from a client that never published a byte. This is the single most likely way to cause an incident while "only reading", and the fix is trivial: a random, obviously-diagnostic client ID, different on every run.

**A persistent session leaves the broker holding a queue for you.** Connect with a session the broker is asked to keep, subscribe to `#` at QoS 1, then close the laptop. The broker now queues every message on the site for a subscriber that is not coming back, until something fills. On a busy broker that is a disk, and the outage will be blamed on the broker. Connect with a clean session and subscribe at QoS 0 and there is nothing to store: at QoS 0 the broker forwards and forgets, which is exactly what a diagnostic wants.

So "read-only" for a subscriber is three properties, not one: it does not publish, it does not take anyone's identity, and it leaves no session state behind.

## What the tree tells you before you write any code

Payload shape first. MQTT 3.1.1 has no content type at all — the payload is bytes and the meaning is a convention between two parties who may both have left the company. MQTT 5 adds a content type and a payload format indicator, which are worth reading when present and absent more often than not. Everything else is inspection: JSON with which keys, a bare decimal, a fixed-width binary record, or one message carrying twenty sensors.

Then publish rate against topic design. A gateway republishing its entire state every second onto one topic per point is a different integration, costed differently, from one publishing a change of value onto a topic per device. Counts and rates per branch are how you tell, in a minute, without asking anyone.

Then the will message, if you catch a disconnection. A last-will topic is the integrator writing down what they thought failure looked like, and it is often the only design documentation that survived.

What to do with all that, on the Niagara side, has its own notes: the stock driver's limits in [what the Niagara MQTT driver will and will not do](https://plantroomlabs.com/notes/niagara-mqtt-driver/), and decoding, topic design and buffering in [bringing LoRaWAN and MQTT data into a Niagara station](https://plantroomlabs.com/notes/lorawan-and-mqtt-into-a-niagara-station/).

We publish a small read-only tap that does the observation described here — tree, counts, payload shapes, last values and retained topics that stopped updating: [mqtt-tap](https://plantroomlabs.com/tools/mqtt-tap/). It is free, it never publishes, and the limits above are its limits too.
