MQTTο
MQTT is a lightweight publish-subscribe messaging protocol designed for IoT applications and real-time communication over unreliable networks. It operates on a broker-based architecture where clients connect to a central broker to exchange messages on named topics.
Overviewο
MQTT communication involves three roles:
Publisher β sends messages to the broker on a topic
Broker β receives messages from publishers, processes them, and forwards them to subscribers
Subscriber β receives messages on topics it has subscribed to
Note
A client can act as both a publisher and subscriber simultaneously β communication is bidirectional. A client can publish and subscribe to the same topic at the same time. This is useful for debugging (publish a message and verify it arrives back), shared state (multiple devices watching and updating the same topic), and loopback/smoke testing. There is no built-in mechanism to filter out your own messages β if you need that, handle it in your application, typically by embedding your Client ID in the payload and checking it on receipt.
A popular open-source broker is Mosquitto, typically run as a standalone application on a server or computer.
(Pub/Sub)"] B["Broker
(Mosquitto)"] C["Client B
(Pub/Sub)"] A -->|PUBLISH| B B -->|PUBLISH| C C -->|SUBSCRIBE| B B -->|SUBSCRIBE| A style B fill:#e1f5ff style A fill:#fff9c4 style C fill:#fff9c4
Packetsο
Packet Structureο
All MQTT packets share a common structure:
Fixed header (mandatory, minimum 2 bytes):
Byte 1: control field β packet type (upper 4 bits) and flags (lower 4 bits)
Bytes 1-4: remaining length field β encodes the length of the variable header and payload using a variable-length encoding scheme
Variable header (present in some packet types) β contains packet-specific fields such as packet identifiers or protocol name
Payload (optional) β the message data
Key limits:
Minimum packet size: 2 bytes (fixed header only)
Maximum packet size: 256 MB (defined by variable-length encoding limit)
Payload format: any binary data, but commonly ASCII-encoded JSON, XML, or plain text
Connecting to the Brokerο
All clients must connect to the broker before doing anything else. This is a two-packet exchange.
checks Client ID,
restores session if applicable B->>C: CONNACK (SessionPresent, ReturnCode) note over C,B: Session active C->>B: DISCONNECT (clean close β Will NOT sent)
CONNECT Packet Fieldsο
Field |
Type |
Description |
|---|---|---|
|
string |
Unique identifier for this client. If blank, the broker generates one β but persistent sessions are then unavailable. |
|
boolean |
|
|
integer |
Maximum interval in seconds between transmissions. Client must send data or a |
|
string |
Optional credentials. Sent in plaintext β always use TLS in production. |
|
message |
Stored by broker at connect time. Published to a specified topic only on an unclean disconnect (crash, power loss). Used to signal device failure. |
CONNACK Responseο
The broker replies with a CONNACK containing:
Session Present flag β
1if stored session state was found for this Client ID (only relevant whencleanSession=false)Return code:
0β connection accepted1β unacceptable protocol version2β identifier rejected3β server unavailable4β bad username or password5β not authorised
Persistent Sessionsο
When cleanSession=false, the broker maintains a persistent session for the client, storing:
The clientβs subscriptions
Undelivered QoS 1 and QoS 2 messages received while offline
Partially acknowledged QoS 2 in-flight message state
On reconnect, the broker hands all of this back. The Session Present flag in CONNACK tells the client whether to re-subscribe (0) or trust its subscriptions are already registered (1).
Note
QoS 0 messages are never stored for offline clients, even in a persistent session.
The only way to clear stored session state is to reconnect with cleanSession=true.
Topics and Subscriptionsο
Topics are UTF-8 strings organised into a hierarchy using / as a level separator. They are case sensitive and require no pre-registration.
factory/line1/sensor/temperature
home/livingroom/light/status
vehicles/truck42/gps/location
Wildcardsο
Wildcard |
Type |
Behaviour |
|---|---|---|
|
Single-level |
Matches exactly one topic level |
|
Multi-level |
Matches all levels from that point down. Must be the last character in the filter. |
Example β ``factory/+/sensor/temperature``:
β factory/line1/sensor/temperature
β factory/line2/sensor/temperature
β factory/line1/motor/temperature (wrong third level)
Example β ``factory/line1/#``:
β factory/line1/sensor/temperature
β factory/line1/motor/speed/max
β factory/line2/sensor/temperature (wrong second level)
Wildcards can be combined: factory/+/sensor/# is valid.
SUBSCRIBE / SUBACKο
A SUBSCRIBE packet contains one or more topic filters each paired with a requested QoS (the maximum the client wants to receive). The broker replies with SUBACK β one return code per filter: granted QoS (0, 1, or 2), or 0x80 = refused.
A client removes subscriptions with UNSUBSCRIBE β broker confirms with UNSUBACK.
System Topicsο
Topics beginning with $ are reserved for broker internals (e.g. $SYS/ for broker statistics). The # and + wildcards do not match $ topics β you must subscribe to $SYS/# explicitly.
Key Rulesο
Topics are case sensitive β
Sensor/Tempβsensor/tempA leading
/creates an empty first level β usually a design mistakeOverlapping subscriptions are valid; matching messages may be delivered more than once
#alone subscribes to every non-$topic on the broker
Quality of Service (QoS)ο
QoS controls the delivery guarantee for a message. Publisher and subscriber set QoS independently on a per-message/per-topic basis (not at the client level). The delivered QoS is always min(publisher QoS, subscriber requested QoS) β the broker downgrades silently with no notification.
Note
QoS is set per-publish (in each PUBLISH packet) and per-topic filter (in each SUBSCRIBE request), not at the client level. Different topics from the same client can use different QoS levels.
Level |
Guarantee |
Packet exchange |
Duplicates? |
|---|---|---|---|
QoS 0 |
At most once |
|
No |
QoS 1 |
At least once |
|
Possible |
QoS 2 |
Exactly once |
|
No |
QoS 0 β At Most Onceο
Fire and forget. No acknowledgement, no retry. Suitable for frequent sensor readings where an occasional missed message is acceptable.
Message lost if dropped.
QoS 1 β At Least Onceο
Sender retransmits until PUBACK is received. If the PUBACK is lost in transit, the message is sent again β the receiver may process a duplicate. Message handling must be idempotent (safe to apply twice, e.g. βset temperature to 21Β°Cβ) or deduplicated in the application.
Retransmits if no PUBACK.
QoS 2 β Exactly Onceο
A four-packet handshake guarantees delivery with no duplicates.
Note
The message is forwarded to subscribers at the PUBREL step, not when PUBLISH arrives.
This is what prevents duplicates: even if PUBLISH is retransmitted, the broker recognises
the message ID and re-sends PUBREC without forwarding again.
When to Use Each Levelο
QoS 0 β frequent telemetry, live sensor readings, anything where the next update arrives shortly
QoS 1 β most common choice; use when message loss is unacceptable and processing is idempotent
QoS 2 β financial events, physical actuator commands, any operation where duplicates cause real harm
Retained Messagesο
A retained message is a normal MQTT message with retain=true. The broker stores it as the last known value for that topic and delivers it instantly to any future subscriber β before any live messages arrive.
Only one retained message is stored per topic β each new retained publish replaces the previous one.
Clearing a Retained Messageο
Publish a zero-byte payload with retain=true to the same topic. The broker discards the stored value and new subscribers receive nothing.
Birth and Last Will Patternο
Combine retained messages with the Will (see Connecting section) to track device presence:
On connect β PUBLISH "online" retain=true β devices/42/status
Will msg β PUBLISH "offline" retain=true β devices/42/status
Any subscriber gets the current presence state immediately on subscribe, regardless of when they connect.
Key Rulesο
Retained messages are per topic on the broker β distinct from persistent sessions which are per client
Wildcard subscriptions receive a burst of all matching retained messages on subscribe β useful for dashboards
QoS negotiation still applies when a retained message is delivered to a new subscriber
Retained messages survive broker restarts if the broker is configured to persist them
Good fit: state/status topics. Poor fit: event topics (a stale retained event delivered out of context can cause unintended behaviour)
Quick Referenceο
Packet |
Purpose |
|---|---|
|
Client β Broker. Opens a session. Contains ClientID, credentials, cleanSession, keepAlive, Will. |
|
Broker β Client. Session accepted or refused. Contains SessionPresent and ReturnCode. |
|
Either direction. Carries topic, payload, QoS, retain flag. |
|
QoS 1 acknowledgement. |
|
QoS 2 step 1 reply β message received and stored. |
|
QoS 2 step 2 β sender releases message for delivery. |
|
QoS 2 step 3 reply β exchange complete. |
|
Client β Broker. One or more topic filters with requested QoS. |
|
Broker β Client. Granted QoS per filter, or 0x80 = refused. |
|
Client β Broker. Remove topic filters. |
|
Broker β Client. Confirms removal. |
|
Client β Broker. Keep-alive heartbeat when no data is flowing. |
|
Broker β Client. Heartbeat reply. |
|
Client β Broker. Clean close β Will message is NOT sent. |
MQTT 5.0ο
MQTT 5.0 introduces several modern features for production systems.
Message Expiryο
Set a TTL on a message (in seconds). If itβs still sitting in the broker undelivered after that time, itβs discarded. Stops stale data reaching late-joining clients.
client.publish("sensors/temp", payload="22.4", properties={"MessageExpiryInterval": 30})
# If the subscriber isn't connected within 30s, it never receives this
Reason Codesο
Every CONNACK, PUBACK, SUBACK etc. now carries a numeric reason code. Instead of a silent failure you get something specific and actionable:
0x00 Success
0x87 Not Authorized
0x97 Quota Exceeded
0x9E Subscription Identifiers Not Supported
User Propertiesο
Arbitrary key-value string pairs attachable to any packet. Think HTTP headers for MQTT β useful for routing metadata, trace IDs, content-type hints without touching the payload.
properties = {"user_properties": [("trace-id", "abc-123"), ("region", "eu-west")]}
client.publish("sensors/temp", payload="22.4", properties=properties)
Session and Will Improvementsο
Will messages can now be delayed, so a brief disconnect doesnβt immediately fire your βdevice offlineβ alert. Set WillDelayInterval to only trigger after a device has been gone for a defined period (e.g. 60 seconds).
Note
Use MQTT 5.0 for any new project. v3.1.1 is still everywhere in legacy systems but 5.0 is clearly the path forward.
Securityο
Plain MQTT is unauthenticated and unencrypted by default. For production you need all three layers.
Transport (TLS)ο
TLS (Transport Layer Security) is the same encryption that underpins HTTPS. It establishes an encrypted tunnel between client and broker: the broker presents a certificate to prove its identity, and all data flowing through the connection is encrypted so it canβt be read or tampered with in transit.
Mutual TLS (mTLS) goes further β the client also presents a certificate, so the broker can verify device identity without passwords. Common in device fleets where each device is issued its own cert at manufacture.
When to use TLS or mTLS in MQTT:
One-way TLS: Device verifies the brokerβs certificate. Use when the brokerβs identity is important but devices donβt need authentication.
mTLS: Both device and broker authenticate each other. Use when the broker needs to reject unknown or revoked devices.
Port 1883 β plaintext, avoid in production
Port 8883 β TLS encrypted, use this
Authenticationο
MQTT 3.x supports username/password in the CONNECT packet. Combined with TLS (which prevents credentials being intercepted) itβs adequate for many cases. MQTT 5.0 adds Enhanced Authentication for SASL-style challenge/response flows (OAuth, Kerberos etc). Client certificate authentication via mTLS can replace passwords entirely β the cert is the identity.
Mosquitto: Passwords and ACLsο
To disable anonymous access and enforce per-client credentials, configure Mosquitto with a password file and an ACL file.
Step 1 β create the password file:
# Create a new file and add the first user (-c = create)
mosquitto_passwd -c /etc/mosquitto/passwd device123
# Add further users (omit -c to avoid overwriting the file)
mosquitto_passwd /etc/mosquitto/passwd dashboard
Step 2 β point mosquitto.conf at the password and ACL files:
# /etc/mosquitto/mosquitto.conf
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
With allow_anonymous false any client that does not present valid credentials is refused at the CONNECT stage.
Step 3 β write the ACL file:
# /etc/mosquitto/acl
# --- Global rules (apply to every authenticated client) ---
topic read public/#
# --- Per-user rules ---
user device123
topic readwrite sensors/device123/#
topic read commands/device123/#
user dashboard
topic read sensors/#
# --- Pattern rules (apply to all users regardless of position) ---
# %u = username, %c = client ID
pattern readwrite devices/%u/#
ACL File Rulesο
Default deny β any topic not covered by an explicit
read,write, orreadwriterule is denied. There is no need to add explicit deny-all entries.Deny takes precedence β
denyrules are evaluated before permissive rules. A singledenyline blocks access even when a broaderreadwriteor wildcard rule would otherwise allow it.User scope β a
user <name>line begins a per-user block. Alltopiclines that follow apply only to that user until the nextuserdeclaration (or end of file). Topics listed before anyuserdeclaration are global and apply to every authenticated client.Pattern rules β lines beginning with
patternuse%u(username) and%c(client ID) as substitution variables. Pattern rules apply to all users regardless of where they appear relative touserblocks.Access types:
readSubscribe and receive messages
writePublish messages
readwriteBoth (default when type is omitted)
denyExplicitly block β takes priority over allow
Example showing deny override:
user ops
topic readwrite sensors/# # allows all sensor topics β¦
topic deny sensors/secret # β¦ except this one
Production Checklistο
TLS on 8883, disable plaintext 1883
Unique credentials per client (not one shared password)
ACLs scoped to only what each client needs
Rotate credentials; donβt hardcode them
Broker Topologyο
One broker is the most common setup for smaller systems β all clients connect to it and it handles all routing.
Multiple brokers are used when you need:
- Scale
Broker clustering (supported by HiveMQ, EMQX etc.) runs multiple nodes as one logical broker. Clients connect to any node; the cluster handles internal routing.
- Bridging
Two separate brokers can be linked so messages on one are forwarded to the other. Common in edge/cloud architectures:
[Factory devices] β [Edge broker] --bridge--> [Cloud broker] β [Dashboard]
The factory devices never talk directly to the cloud.
- Isolation
Some enterprises run separate brokers per site or business unit for security/compliance, bridging only what needs to cross boundaries.
Note
The broker is always the hub β clients never talk directly to each other, even in multi-broker setups. The topology changes, but the fundamental rule (publish to broker, broker routes to subscribers) does not.