Logs tell you what a client thinks it sent. A packet capture tells you what actually went over the wire. Wireshark has a full MQTT dissector built in: it decodes CONNECT flags, topic names, QoS, packet identifiers, MQTT 5 properties and reason codes, so you can see in seconds whether a missing message was never published, never acknowledged or never delivered.
This guide covers capturing MQTT traffic, the display filters you will use every day, decrypting TLS on port 8883, and a few tshark one-liners for when you are on a server without a GUI.
Capture the right traffic
Start Wireshark on the machine running the client (or the broker, if you control it) and pick the interface that carries the traffic: your Wi-Fi or Ethernet adapter for remote brokers, the loopback interface for a broker on localhost.
Use a capture filter so the file stays small. Capture filters use BPF syntax and are set before you press start:
tcp port 1883 # plain MQTT
tcp port 1883 or tcp port 8883 # plain and TLS
host broker.connectmq.com # everything to and from the brokerThen generate the traffic you want to analyse — for example a subscriber and a publisher on a unique topic:
mosquitto_sub -h broker.connectmq.com -p 1883 -t 'testmqtt/wireshark/#' -q 1 -v &
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/wireshark/hello' -q 1 -m 'ping'If you capture on a server, tcpdump -w is often easier and you open the file in Wireshark afterwards; see capturing MQTT with tcpdump.
The display filters you need
Display filters are applied after the capture and use Wireshark's field names. The MQTT dissector's main fields:
| Goal | Display filter |
|---|---|
| All MQTT packets | mqtt |
| Only CONNECT / CONNACK | mqtt.msgtype == 1 || mqtt.msgtype == 2 |
| Only PUBLISH | mqtt.msgtype == 3 |
| A topic or topic prefix | mqtt.topic contains "testmqtt/wireshark" |
| One client's CONNECT | mqtt.clientid == "sensor-01" |
| Refused connections | mqtt.conack.val != 0 |
| Retained publishes | mqtt.retain == 1 |
| QoS 1 and 2 publishes | mqtt.msgtype == 3 && mqtt.qos > 0 |
| Keep-alive traffic | mqtt.msgtype == 12 || mqtt.msgtype == 13 |
| Connections reset | tcp.flags.reset == 1 |
| TCP retransmissions | tcp.analysis.retransmission |
The numeric values of mqtt.msgtype are the MQTT control packet types:
| Value | Packet | Value | Packet |
|---|---|---|---|
| 1 | CONNECT | 8 | SUBSCRIBE |
| 2 | CONNACK | 9 | SUBACK |
| 3 | PUBLISH | 10 | UNSUBSCRIBE |
| 4 | PUBACK | 11 | UNSUBACK |
| 5 | PUBREC | 12 | PINGREQ |
| 6 | PUBREL | 13 | PINGRESP |
| 7 | PUBCOMP | 14 | DISCONNECT |
Type 15 is AUTH (MQTT 5 only). For the byte-level layout behind these fields, see MQTT packet structure.
Reading a CONNECT and CONNACK
Select a CONNECT packet and expand MQ Telemetry Transport Protocol. You will see:
- Protocol name and version —
MQTTwith version 4 (3.1.1) or 5. A version the broker does not accept is answered with an unacceptable protocol version error. - Connect flags — clean session / clean start, will flag, will QoS, will retain, and whether username and password are present.
- Keep alive — the interval in seconds the client promised.
- Client ID, and on plain port 1883, the username and password in clear text.
- MQTT 5 properties such as session expiry interval, receive maximum and maximum packet size.
The CONNACK that follows carries the result. On MQTT 3.1.1 it is a return code (0 means accepted); on MQTT 5 it is a reason code such as 0x86 Bad User Name or Password or 0x87 Not authorized, sometimes with a reason string. Look up any value with the reason code tool. The full field-by-field breakdown is in the MQTT CONNECT packet.
If there is no CONNACK at all and the broker sends a TCP RST or FIN instead, the broker rejected the connection before it got as far as MQTT — often a TLS listener receiving plain MQTT, a malformed packet or a connection limit.
Following one message end to end
To prove where a message got lost, follow it through the capture:
- Filter
mqtt.topic contains "testmqtt/wireshark"and find the publisher's PUBLISH. Note its Message Identifier for QoS 1 or 2. - Filter
mqtt.msgid == <id>to see the PUBACK (QoS 1) or the PUBREC / PUBREL / PUBCOMP sequence (QoS 2). No PUBACK means the broker never accepted it — check ACLs and payload size. - If you captured on the subscriber side too, look for the PUBLISH the broker delivered. The packet identifier differs, because each hop has its own.
Statistics → Flow Graph with "Displayed packets" draws the exchange as a sequence diagram, which is the quickest way to spot a missing acknowledgement.
Decrypting MQTT over TLS (port 8883)
On port 8883 you will only see TLS records until Wireshark has the session keys. The standard way to get them is a key log file, which many TLS libraries can write.
Python (paho-mqtt 2.x) — set keylog_filename on the SSL context:
import ssl
import paho.mqtt.client as mqtt
ctx = ssl.create_default_context()
ctx.keylog_filename = "/tmp/mqtt-tls-keys.log" # Python 3.8+
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.tls_set_context(ctx)
client.connect("broker.connectmq.com", 8883)
client.loop_start()
client.publish("testmqtt/wireshark/tls", "hello over TLS", qos=1).wait_for_publish()
client.loop_stop()
client.disconnect()Node.js (MQTT.js) — use the built-in flag, no code change needed:
node --tls-keylog=/tmp/mqtt-tls-keys.log publish.jsBrowsers and curl honour the SSLKEYLOGFILE environment variable. Then in Wireshark open Edit → Preferences → Protocols → TLS and set (Pre)-Master-Secret log filename to the key log path. The TLS records on 8883 turn into decoded MQTT packets, for TLS 1.2 and 1.3 alike.
If the TLS handshake itself fails, you do not need decryption: the alert record (for example Unknown CA or Handshake Failure) is visible in plain sight. See TLS handshake failed.
Non-standard ports and WebSockets
Wireshark only assumes MQTT on its registered ports. If your broker listens on, say, 11883, right-click a packet, choose Decode As…, and map that TCP port to MQTT.
For MQTT over WebSockets (8083 / 8084), Wireshark shows the HTTP upgrade request and the WebSocket frames; the MQTT packets are inside binary frames. For browser apps, it is usually faster to read those frames in the browser itself — see debugging MQTT in Chrome DevTools.
tshark one-liners
tshark is Wireshark's command-line twin and uses the same filters:
# Live: print time, packet type, topic and payload
tshark -i any -f "tcp port 1883" -Y mqtt \
-T fields -e frame.time_relative -e mqtt.msgtype -e mqtt.topic -e mqtt.msg
# From a file: count PUBLISH packets per topic
tshark -r mqtt.pcap -Y "mqtt.msgtype == 3" -T fields -e mqtt.topic | sort | uniq -c | sort -rn
# Gaps between keep-alive pings (look for intervals near the keep-alive limit)
tshark -r mqtt.pcap -Y "mqtt.msgtype == 12" -T fields -e frame.time_delta_displayedThe last one is handy for keep-alive timeouts: if the gaps exceed 1.5 times the keep-alive value, the broker is entitled to disconnect the client.
What to look for, by symptom
| Symptom | What the capture shows |
|---|---|
| Client "can't connect" | CONNECT followed by CONNACK with non-zero code, or RST with no CONNACK |
| Messages lost at QoS 1 | PUBLISH without PUBACK, then a duplicate (DUP flag set) after reconnect |
| Random disconnects | Gaps in PINGREQ, TCP retransmissions, then FIN or RST from one side |
| Big messages vanish | Large PUBLISH followed by the broker closing the connection — compare with the maximum packet size, and check sizes with the packet size calculator |
| Subscriber gets nothing | SUBACK with failure code 0x80 or a topic filter that does not match |
To reproduce a case quickly, run the capture while you publish from the online MQTT client and watch the packets arrive in real time.
Wireshark replaces guessing with evidence. Capture narrowly, filter by packet type and topic, decrypt TLS only when you need to, and keep a capture of a known-good session around — comparing a broken capture with a working one is often the fastest diagnosis of all.
Frequently asked questions
What is the Wireshark display filter for MQTT?
Use 'mqtt' to show all decoded MQTT packets. Narrow it with fields such as mqtt.msgtype == 3 for PUBLISH, mqtt.topic contains "sensors" for a topic, or mqtt.clientid == "sensor-01" for one client's CONNECT.
Why does Wireshark show TLS instead of MQTT on port 8883?
The MQTT packets are encrypted. Wireshark can only decode them if you give it the TLS session keys: make the client write a key log file (SSLKEYLOGFILE, Python's keylog_filename, Node's --tls-keylog) and point Wireshark's TLS preferences to that file.
Why is my MQTT traffic shown as plain TCP data?
Wireshark decodes MQTT automatically only on the standard port 1883 (and 8883 after decryption). For any other port, right-click a packet, choose Decode As and set the TCP port to MQTT.

