Paste the bytes and the decoder splits them into fields: packet type, flags, remaining length, then whatever that packet type carries. Topic names, QoS, packet identifiers, MQTT 5 properties, reason codes and the payload.
It reads a whole TCP stream, not one packet. Paste a capture that runs from CONNECT to DISCONNECT and you get every packet in order.
Hex from Wireshark’s MQTT payload. Several packets from one TCP stream are fine.
A CONNECT states its version, and everything after it follows. On its own, a PUBLISH looks the same in both versions except for the property block, so set the version by hand when you paste a single packet.
Getting the bytes out of Wireshark
Filter on mqtt, click a packet, right-click the MQ Telemetry Transport layer in the detail pane, then Copy → …as a Hex Stream. Paste that here.
Copy from the TCP layer instead and you get the TCP header with it. The decoder will read the first two octets as a packet type and remaining length and produce nonsense, which is a good sign you copied the wrong layer.
If you have several MQTT packets in one TCP segment, follow the stream and copy the whole payload. The decoder walks it packet by packet using each remaining length field.
The two octets that matter most
Every MQTT packet starts the same way. One octet of type and flags, then a length.
The first nibble is the packet type. The second nibble is flags, and for most packet types the standard fixes what those flags must be — zero for CONNECT, CONNACK, PUBACK and the rest, but 0010 for PUBREL, SUBSCRIBE and UNSUBSCRIBE. A broker that receives the wrong value has to treat the packet as malformed and close the connection. The decoder checks this on every packet, because it is a common bug in hand-rolled clients.
PUBLISH is the exception. Its flag nibble carries real information:
| Bit | Name | Meaning |
|---|---|---|
| 3 | DUP | This is a redelivery. Must be 0 on QoS 0 |
| 2–1 | QoS | 0 at most once, 1 at least once, 2 exactly once. 3 is invalid |
| 0 | RETAIN | The broker keeps this message as the last known value for the topic |
Then comes the remaining length: a variable byte integer, one to four octets, seven bits of value each with the top bit meaning “another octet follows”. It counts everything after itself, so the packet on the wire is the length plus the header. That encoding is why a 127-byte message costs two header octets and a 128-byte message costs three.
What each packet type shows
CONNECT is the one worth reading closely. Protocol name, protocol level, the connect flags byte broken into user name, password, will retain, will QoS, will flag and clean start, then keep alive. The payload gives you the client identifier, the will topic and payload, and the credentials.
The keep alive value tells you when the broker gives up: the standard allows one and a half times the value before it declares the client dead. A keep alive of 60 means the connection survives 90 seconds of silence.
CONNACK carries the session present bit and the return code. Session present set means the broker restored an old session, so the client keeps its subscriptions and any queued QoS 1 and 2 messages. Clients that resubscribe blindly on every connect are usually clients that never read this bit.
PUBLISH shows the topic, the packet identifier when QoS is above zero, and the payload. The payload is rendered three ways: parsed as JSON when it is JSON, as text, and as hex.
SUBSCRIBE lists every topic filter with its requested QoS. In MQTT 5 each filter also carries No Local, Retain As Published and Retain Handling, all decoded.
SUBACK returns one reason code per filter, in the same order as the request. This is where a subscription silently fails: the broker answers 0x80 for a filter it will not grant, the client logs a successful subscribe, and nobody notices until a message never arrives.
DISCONNECT in MQTT 3.1.1 is two bytes and always means a clean close. In MQTT 5 it carries a reason code in both directions, so the broker can tell you why it dropped you.
MQTT 5 properties
Version 5 adds a property block to nearly every packet: a length, then a list of identifier and value pairs. The decoder resolves all of them by name and type.
| ID | Property | Where it usually matters |
|---|---|---|
| 1 | Payload format indicator | 1 means the payload is UTF-8 text |
| 2 | Message expiry interval | The broker drops the queued message after this many seconds |
| 3 | Content type | Free-form MIME string, usually application/json |
| 8 | Response topic | Request and response pattern |
| 9 | Correlation data | Ties a response back to its request |
| 11 | Subscription identifier | Which subscription caused this delivery |
| 17 | Session expiry interval | How long the broker keeps the session after disconnect |
| 19 | Server keep alive | The broker overriding the client’s keep alive |
| 24 | Will delay interval | Wait before publishing the will, so a quick reconnect avoids it |
| 33 | Receive maximum | How many unacknowledged QoS 1 and 2 messages are allowed in flight |
| 34, 35 | Topic alias maximum, topic alias | Replaces a long topic string with a two-byte number |
| 36–42 | Maximum QoS, retain available, wildcard available and so on | The broker declaring what it supports in CONNACK |
| 38 | User property | Key and value pair, repeatable, application defined |
| 39 | Maximum packet size | Publish above this and the broker disconnects you |
The CONNACK properties are the useful ones on a new broker. They tell you the real limits before you design around limits that are not there.
Picking the version
A CONNECT states its protocol level, and the decoder carries that through the rest of the paste. Paste a session and the version is handled for you.
A single PUBLISH is ambiguous. In MQTT 5 the octet after the topic and packet identifier is the property length; in 3.1.1 that same octet is already payload. Nothing in the packet says which. Set the version by hand when you paste one packet on its own, or paste the CONNECT along with it.
What the checks catch
The decoder marks a packet as malformed rather than guessing past the problem.
| Finding | Why it matters |
|---|---|
| Flag nibble wrong for the packet type | A conforming broker closes the connection |
| Remaining length longer than the data | Truncated capture, or a length field the client got wrong |
| QoS 3 | Both QoS bits set. Not a valid value |
| DUP set on QoS 0 | There is nothing to redeliver at QoS 0 |
| Packet identifier zero | Not allowed on any packet that carries one |
| Wildcards in a publish topic | + and # belong in filters, never in a published topic |
| Reserved bit 0 of the connect flags set | The broker must reject the CONNECT |
| Will QoS or will retain set with no will flag | Contradictory connect flags |
| Password without a user name | Not allowed in 3.1.1 |
| Empty client identifier with a persistent session | Nothing to resume the session against |
# not in the last position of a filter | The multi-level wildcard only works at the end |
| Property block running past the end of the packet | A length field is wrong somewhere |
| Credentials in the CONNECT | They are in clear text unless TLS is underneath |
That last one is not a protocol error. It is a note, because it is worth seeing written down when you are looking at a plant network capture: the user name and password are plainly readable in the hex, and TCP port 1883 carries them that way by design. Port 8883 with TLS is the answer, not obfuscation.
FAQ
Does this send my packets anywhere? No. The decoding runs in the page. There is no request to any server, and nothing is stored.
Can I decode a whole capture file? Not a pcap directly. Export the payload bytes from Wireshark and paste them. Following one TCP stream and copying its payload gives you the full client session, and the decoder walks it packet by packet.
Why does my PUBLISH show a strange property block? Because it is being read as MQTT 5 when it is 3.1.1, or the other way round. Switch the version selector. The byte map makes it obvious which is right: in the wrong version the property fields swallow the start of your payload.
What does the broker do with a retained message that has an empty payload? It deletes the retained message on that topic. The decoder flags it, because an empty retained publish is easy to mistake for a bug when it is the documented way to clear a retained value.
My SUBACK returns 0x80. What now? The broker refused that filter. Usually an access control rule, sometimes a filter the broker considers invalid. The reason codes in MQTT 5 are more specific than the single failure code of 3.1.1, which is one good argument for moving to version 5.
Does it handle Sparkplug B payloads? It shows the payload as hex and text, but Sparkplug B payloads are protobuf, so they will not read as text. The topic structure will decode normally, which is often enough to see whether the namespace, group and edge node identifiers are what you expect.
Why does one packet show as several, or several as one? The decoder trusts the remaining length field. If a length is wrong, the boundary between packets moves and everything after it decodes as rubbish. That is the first thing to check when the output stops making sense partway through a stream.
