MQTT Packet Decoder — Decode MQTT 3.1.1 and 5.0 Hex Online

By | August 19, 2026

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.

Packet input
Hex from Wireshark’s MQTT payload. Several packets from one TCP stream are fine.

Load an example:









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.






0 bytes
Paste MQTT bytes and press Decode. Ctrl+Enter works too.

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:

BitNameMeaning
3DUPThis is a redelivery. Must be 0 on QoS 0
2–1QoS0 at most once, 1 at least once, 2 exactly once. 3 is invalid
0RETAINThe 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.

IDPropertyWhere it usually matters
1Payload format indicator1 means the payload is UTF-8 text
2Message expiry intervalThe broker drops the queued message after this many seconds
3Content typeFree-form MIME string, usually application/json
8Response topicRequest and response pattern
9Correlation dataTies a response back to its request
11Subscription identifierWhich subscription caused this delivery
17Session expiry intervalHow long the broker keeps the session after disconnect
19Server keep aliveThe broker overriding the client’s keep alive
24Will delay intervalWait before publishing the will, so a quick reconnect avoids it
33Receive maximumHow many unacknowledged QoS 1 and 2 messages are allowed in flight
34, 35Topic alias maximum, topic aliasReplaces a long topic string with a two-byte number
36–42Maximum QoS, retain available, wildcard available and so onThe broker declaring what it supports in CONNACK
38User propertyKey and value pair, repeatable, application defined
39Maximum packet sizePublish 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.

FindingWhy it matters
Flag nibble wrong for the packet typeA conforming broker closes the connection
Remaining length longer than the dataTruncated capture, or a length field the client got wrong
QoS 3Both QoS bits set. Not a valid value
DUP set on QoS 0There is nothing to redeliver at QoS 0
Packet identifier zeroNot 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 setThe broker must reject the CONNECT
Will QoS or will retain set with no will flagContradictory connect flags
Password without a user nameNot allowed in 3.1.1
Empty client identifier with a persistent sessionNothing to resume the session against
# not in the last position of a filterThe multi-level wildcard only works at the end
Property block running past the end of the packetA length field is wrong somewhere
Credentials in the CONNECTThey 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.

Author: Zakaria El Intissar

I've spent 13 years in power system automation, electrical protection, and SCADA communication, as an automation and industrial computing engineer. ScadaProtocols.com is where I turn what I've learned on site into plain guides and working tools — so other engineers can decode, analyze, and troubleshoot industrial communication protocols without the guesswork.