In 1999, two engineers named Andy Stanford-Clark and Arlen Nipper faced a brutal engineering problem:
They needed to monitor crude oil pipelines stretching across hundreds of miles of remote desert in the American Southwest.
The only communication channel available was fragile, low-bandwidth satellite links that billed by the kilobyte. The remote battery-powered sensors had tiny 8-bit microcontrollers and minuscule memory buffers.
If they had used HTTP, the repetitive plain-text headers alone would have burned thousands of dollars in satellite bandwidth every day while draining the device batteries within a week.
To solve that crisis, they designed a lean binary wire protocol and named it:
MQTT (Message Queuing Telemetry Transport).
Fast forward to today: MQTT powers hundreds of millions of connected devices across the planet:
- Every electric vehicle streaming real-time battery and GPS telemetry to cloud fleets.
- Every smart lightbulb and sensor running through Home Assistant and Zigbee networks.
- Industrial robotics and manufacturing SCADA assembly lines.
- Even mobile messaging giants like Facebook Messenger, which famously ported mobile push notifications to MQTT to save battery on low-end cellular phones.
Yet despite its global ubiquity, almost every software engineer who encounters MQTT for the first time carries one massive, fundamental misunderstanding:
MQTT does not actually have message queues.
Despite the letters MQ in its name, MQTT does not buffer messages in FIFO queues like RabbitMQ, nor does it store distributed partition logs like Apache Kafka.
If a subscriber goes offline without a persistent session, incoming messages simply vanish into thin air.
So what is MQTT, really?
It is an ultra-compact, binary-framed Publish/Subscribe event router that can transmit a live telemetry packet with a fixed header as small as 2 bytes.
To understand why MQTT dominates the connected world, you have to look beneath high-level client libraries and inspect how the binary wire protocol, delivery handshakes, and broker routing actually work under the hood.
Here is the complete mechanics of MQTT.
The Big Misconception: Why MQTT Has No Queues
When software engineers hear "Message Queuing", they naturally picture tools like RabbitMQ, ActiveMQ, or Amazon SQS:
- A publisher pushes a task to a named queue.
- The queue holds the message in persistent memory.
- A consumer comes online minutes later, pulls the task from the queue, processes it, and acknowledges receipt.
MQTT operates on a completely different paradigm: Space, Time, and Synchronization Decoupling.
[ Sensor / Publisher ] ──> PUBLISH "factory/temp" ──> [ MQTT Broker ] ──> Instant Push ──> [ Subscribers ]
│
(If 0 subscribers are online:
Message is dropped immediately!)
The Spatial Decoupling
In MQTT, publishers and subscribers never know about each other.
A temperature sensor publishing readings to factory/temp has zero knowledge of which services exist, how many dashboards are open, or what IP addresses they live at. It sends its packet to an intermediary called the MQTT Broker (such as EMQX, Mosquitto, or HiveMQ) and immediately returns to sleep.
The Missing Queue
Here is why it is not a queue:
In MQTT, messages are delivered to topics, not to queues.
When a broker receives a PUBLISH packet:
- It inspects its in-memory subscription tree to see who is currently listening to that topic.
- If three clients are subscribed, it immediately clones and forwards the packet to all three clients over their open TCP connections.
- If zero clients are subscribed, the broker discards the message immediately.
There is no default storage. There is no queue waiting for a subscriber to boot up tomorrow.
The only exception is when a client connects with a Persistent Session (CleanSession=false in MQTT 3.1.1, or CleanStart=false with a Session Expiry Interval in MQTT 5.0).
In that specific scenario, the broker buffers unacknowledged QoS 1 and QoS 2 messages specifically for that known ClientId. But the topic itself remains a stateless broadcast channel, not an independent queue.
Anatomy of the 2-Byte Fixed Header Wire Format
Why is MQTT so fast, and why can an IoT sensor run on a single coin-cell battery for five years?
The secret is the binary wire layout.
In HTTP/1.1, sending a single number requires hundreds of bytes of plain-text ASCII:
POST /telemetry HTTP/1.1
Host: api.iot.com
Content-Type: application/json
Content-Length: 18
User-Agent: Mozilla/5.0...
{"temperature":24}
In MQTT, the fixed header requires as little as 2 bytes.
Let us inspect the exact bit layout of an MQTT packet wire frame:
Byte 1: Packet Type & Control Flags
Byte 1 is packed down to individual bits:
Bit: 7 6 5 4 │ 3 │ 2 1 │ 0
[ Packet Type ] │ DUP │ [ QoS ] │ RETAIN
-
Bits 7 to 4 (Packet Type): 4 bits give 16 possible control packet types:
-
0001(1):CONNECT(Client requests connection) -
0010(2):CONNACK(Broker connection acknowledgement) -
0011(3):PUBLISH(Publish message) -
0100(4):PUBACK(Publish acknowledgement for QoS 1) -
1100(12):PINGREQ(Heartbeat ping request) -
1101(13):PINGRESP(Heartbeat ping response) -
1110(14):DISCONNECT(Graceful client disconnect)
-
Bit 3 (DUP Flag): Set to
1when the client or broker is re-attempting delivery of an unacknowledged QoS 1 or QoS 2 packet.-
Bits 2 and 1 (QoS Level):
-
00= Quality of Service 0 (At most once) -
01= Quality of Service 1 (At least once) -
10= Quality of Service 2 (Exactly once)
-
Bit 0 (RETAIN Flag): If set to
1, the broker saves this message as the "last known good value" for this topic. Any new subscriber that connects in the future will receive this message immediately upon subscription.
Byte 2+: The Variable Byte Remaining Length
How does MQTT express the size of the packet without wasting multiple bytes on every small sensor reading?
It uses an ingenious encoding called the Variable Byte Integer:
- Each byte uses 7 bits to encode the numeric length value (
0to127). - The 8th bit (Most Significant Bit, MSB) is the Continuation Bit.
- If the MSB is
0, decoding stops immediately. - If the MSB is
1, it signals that the next byte contains additional length data.
This gives MQTT an astonishing dynamic range:
- For payloads under 128 bytes, the length field takes 1 single byte!
- For larger payloads, it expands up to 4 bytes, supporting payloads up to 268,435,455 bytes (256 MB)!
When a microcontroller sends a temperature reading with QoS 0, the total protocol overhead is literally two hexadecimal bytes:
0x30 0x12
0x30 means: Packet Type 3 (PUBLISH), QoS 0, no retain.
0x12 means: 18 bytes of remaining topic and payload data follow.
Zero ASCII parsing. Zero text string overhead. Pure binary efficiency.
The 3 Quality of Service (QoS) Guarantees
Because IoT devices operate over noisy radio links, cellular networks with tunnel dead-zones, and unstable satellite connections, MQTT provides three distinct delivery guarantees called Quality of Service (QoS) levels.
Many engineers assume that higher QoS is always better. In reality, each QoS level represents a distinct engineering tradeoff between network bandwidth, battery consumption, and delivery certainty.
QoS 0: At Most Once (Fire and Forget)
[ Sender ] ─── PUBLISH ───> [ Receiver ]
QoS 0 is the simplest and fastest delivery mode:
- The sender transmits a single
PUBLISHpacket. - The sender does not request an acknowledgement.
- No packet ID is allocated.
- No retry timer is started.
If the cellular connection drops the instant the packet leaves the radio antenna, the message is lost forever.
When to use QoS 0: High-frequency, ephemeral telemetry. If a weather station publishes wind speed every 2 seconds, losing one reading is irrelevant because a fresh reading will arrive in 2,000 milliseconds. Paying for acknowledgements would only drain the battery.
QoS 1: At Least Once (Acknowledged Delivery)
[ Sender ] ─── PUBLISH (PacketId: 10) ───> [ Receiver ]
[ Sender ] <─── PUBACK (PacketId: 10) ─── [ Receiver ]
QoS 1 guarantees that the receiver gets the message at least once, but introduces the risk of duplicate messages:
- The sender assigns a unique 16-bit
Packet Identifierto thePUBLISHframe. - The sender stores the packet in local memory and starts an acknowledgement timer.
- The receiver accepts the packet, forwards it to its application logic, and returns a
PUBACKcontaining the samePacketId. - When the sender receives
PUBACK, it deletes the packet from memory.
The Duplicate Trap: What happens if the receiver receives the PUBLISH packet, but the network drops right before the PUBACK returns to the sender?
The sender's timer expires. It re-transmits the packet with the DUP flag set to 1. The receiver gets the message a second time!
Crucial Engineering Rule: If you use QoS 1, your backend consumer must be idempotent. Processing the exact same event twice must not corrupt your database or charge a credit card twice.
QoS 2: Exactly Once (The 4-Way Handshake)
[ Sender ] ─── 1. PUBLISH (PacketId: 42) ───> [ Receiver ]
[ Sender ] <─── 2. PUBREC (PacketId: 42) ─── [ Receiver ]
[ Sender ] ─── 3. PUBREL (PacketId: 42) ───> [ Receiver ]
[ Sender ] <─── 4. PUBCOMP (PacketId: 42) ─── [ Receiver ]
QoS 2 is the most sophisticated delivery guarantee in network protocols: it ensures that the message is received exactly once, with zero loss and zero duplicates.
It accomplishes this via a coordinated two-step state machine:
-
PUBLISH: Sender transmits packet with
PacketId: 42. -
PUBREC (Publish Received): Receiver caches
PacketId: 42in its internal state, acknowledging receipt without delivering it to the application yet. -
PUBREL (Publish Release): Sender receives
PUBRECand tells the receiver: "I know you got it. You may now release and process the message." -
PUBCOMP (Publish Complete): Receiver processes the message, deletes the
PacketIdfrom its cache, and sendsPUBCOMP. Both sides finalize the transaction.
QoS 2 eliminates duplicate delivery completely, but requires two complete round-trips (four packets).
When to use QoS 2: Mission-critical commands where duplicate execution is catastrophic: opening a motorized security gate, firing a medical alarm, or decrementing a physical warehouse inventory counter.
Topic Trees and Dynamic Wildcard Routing
MQTT does not use fixed queue names. It routes messages using hierarchical, slash-separated strings called Topic Trees:
factory / building1 / line2 / motor4 / temperature
This topic tree forms a virtual namespace that clients can slice and filter dynamically.
How Brokers Route Topics in Memory
A production MQTT broker handling 500,000 devices cannot execute database queries to determine who gets each message.
Instead, brokers store topic subscriptions in an in-memory Trie (Prefix Tree) Data Structure.
Each forward slash (/) creates a child node in the tree. When a packet arrives for factory/building1/line2/motor4/temperature, the broker traverses the Trie nodes in nanoseconds, identifying every matching client socket directly.
The Two Wildcards
When subscribing to topics, clients can use two special wildcard characters:
1. Single-Level Wildcard: +
The plus symbol (+) matches exactly one level in the topic tree.
For example, this subscription:
factory/+/line2/motor4/temperature
Matches:
factory/building1/line2/motor4/temperaturefactory/building2/line2/motor4/temperature
It will not match factory/building1/subzone/line2/motor4/temperature because that spans two levels.
2. Multi-Level Wildcard: #
The hash symbol (#) matches all remaining sub-levels in the hierarchy. It must always appear as the very last character of a topic filter.
For example:
factory/building1/#
This single subscription captures every sensor reading, motor status, temperature, error code, and diagnostic event generated inside building1.
Retained Messages: The State Latch
In standard Pub/Sub, if a dashboard client boots up at 10:00 AM, and a smart thermostat only publishes its temperature when the temperature changes (say, at 9:55 AM and 10:30 AM), the dashboard would show blank data for 30 minutes!
This is where Retained Messages solve the problem.
When a publisher sends a message with the RETAIN=1 flag:
- The broker delivers the message to all current subscribers.
- The broker saves that exact message on the topic node in memory.
- Whenever a new subscriber registers for that topic in the future, the broker immediately delivers the retained message as an initial state snapshot.
Retained messages turn an ephemeral streaming protocol into an instant state discovery engine.
Last Will and Testament (LWT) & KeepAlive Heartbeats
In a world of wired web servers, when a client closes a connection, the operating system gracefully sends a TCP FIN packet. The server knows immediately that the client disconnected.
In the IoT world, devices rarely disconnect gracefully:
- A vehicle drives into an underground concrete parking garage, losing cell signal instantly.
- A battery runs completely dry in a field sensor.
- A construction worker accidentally severs an Ethernet cable.
In all of those scenarios, no TCP FIN packet is ever sent.
As far as the operating system kernel is concerned, the TCP socket is still open! Without an active probe, the server might wait for hours before TCP keepalive timers realize the socket is dead.
MQTT solves this with a two-part reliability mechanism: KeepAlive Heartbeats and Last Will and Testament (LWT).
Step 1: The Will Registration
When an IoT device first establishes a connection, it includes its "Last Will" inside the initial CONNECT packet:
-
Will Topic:
devices/sensor-42/status -
Will Payload:
{"state": "offline", "reason": "unexpected_disconnect"} -
Will QoS:
1 -
Will Retain:
true
The broker acknowledges the connection with CONNACK and quietly stores the Last Will in memory. As long as the device is healthy, the broker does nothing with this will.
Step 2: The KeepAlive Ping Cycle
The device specifies a KeepAlive interval in seconds (for example, 60).
If the device has not published any telemetry within 60 seconds, it sends a tiny 2-byte PINGREQ packet (0xC0 0x00).
The broker immediately responds with a 2-byte PINGRESP packet (0xD0 0x00).
This 2-byte exchange resets the timer while consuming virtually zero cellular data and keeping stateful NAT firewall ports open across cellular carriers.
Step 3: Sudden Death Detection
If the broker does not receive any packet from the device for 1.5 times the KeepAlive duration (in this case, 90 seconds):
- The broker assumes the device is physically dead.
- The broker forcibly closes the underlying TCP socket.
- The broker automatically publishes the stored Last Will and Testament to
devices/sensor-42/status.
Every monitoring dashboard, mobile app, and backend alert system listening to that topic instantly receives the notification: "state": "offline".
The dead device did not have to send an alert; the broker executed its pre-registered will on its behalf.
And if the device disconnects cleanly on purpose? It sends a DISCONNECT packet (0xE0 0x00). The broker deletes the Last Will from memory without publishing it.
MQTT v5: Shared Subscriptions and Modern Cloud Scaling
For many years, the standard version of MQTT was v3.1.1. While it conquered embedded hardware, modern cloud architectures exposed a major challenge:
How do you load-balance an MQTT topic across a cluster of backend microservices?
In MQTT 3.1.1, if three backend worker instances subscribed to telemetry/#, the broker would send a copy of every message to all three workers.
If 100,000 connected cars were streaming GPS coordinates, every backend worker had to process 100,000 streams simultaneously, destroying horizontal scalability.
The OASIS committee solved this in MQTT version 5.0 by introducing Shared Subscriptions.
How Shared Subscriptions Work
Shared subscriptions bring the concept of Kafka Consumer Groups directly into native MQTT.
Instead of subscribing to the raw topic, backend microservices subscribe using a reserved prefix:
$share / <Group-Name> / <Topic-Filter>
For example:
$share/telemetry-processors/vehicles/+/gps
When three backend instances connect with that shared subscription:
- The broker recognizes the
$share/telemetry-processorsprefix. - It groups the three backend connections into a single logical consumer pool.
- When incoming GPS packets arrive from vehicles, the broker round-robins or load-balances each packet to only ONE worker in the group!
If Worker A is busy, Message 2 goes to Worker B, and Message 3 goes to Worker C.
If traffic surges from 1,000 cars to 1,000,000 cars, you simply spin up ten more container instances of your backend service. The broker distributes the telemetry load across the cluster without needing an external Kafka or RabbitMQ ingestion layer in between!
Architectural Comparison: Choosing the Right Tool
To see where MQTT fits into modern distributed systems, compare it directly against the other major protocols:
| Feature | MQTT | HTTP / REST | WebSockets | Apache Kafka |
|---|---|---|---|---|
| Primary Paradigm | Pub/Sub Event Broker | Request / Response | Bidirectional Stream | Distributed Commit Log |
| Header Overhead | 2 Bytes minimum | 500 to 2,000+ Bytes | 2 to 14 Bytes | Configurable / Batch |
| Transport Protocol | TCP / TLS / WebSockets | TCP / TLS (HTTP/2, 3) | TCP / TLS | TCP / TLS |
| Device Constraint | Extreme (8-bit MCUs) | Poor (requires HTTP stack) | Moderate | None (heavy cloud servers) |
| Delivery Guarantees | QoS 0, 1, 2 | Application level | None (raw TCP) | At least once / Transactions |
| Message Queuing | None (Ephemeral) | None | None | Persistent Distributed Log |
| Dead Device Detection | Native LWT (Last Will) | Health checks only | Socket heartbeat | Heartbeat timeouts |
| Best Used For | Telemetry, smart devices, fleet IoT | CRUD APIs, public web | Browser UI live updates | Big data analytics, event replay |
The Takeaway: The Elegance of Restraint
MQTT is a masterclass in software engineering restraint.
In an industry that constantly adds layers of JSON, GraphQL, and microservice complexity, MQTT succeeded by doing the opposite:
- It stripped headers down to the bare metal. A 2-byte header ensures that battery and bandwidth are never wasted on protocol bureaucracy.
- It decoupled publishers from consumers entirely. Devices never know who is listening, allowing systems to scale from one subscriber to a million without touching the edge firmware.
- It embraced network failure as a first-class reality. Through QoS handshakes and Last Will and Testament, it provides rock-solid reliability over the most unstable physical channels on Earth.
When you look at modern IoT architectures, the biggest mistake is treating MQTT like a simple HTTP replacement or assuming it will manage durable message queues on its own.
Understanding the binary framing, the QoS state machines, and the broker routing tree transforms MQTT from a mysterious black box into one of the most powerful distributed tools in your engineering arsenal.
Have you worked with MQTT in production smart home devices, vehicle tracking, or industrial telemetry? What was the biggest architectural hurdle you faced when bridging low-bandwidth edge MQTT streams into high-throughput cloud databases?
References & Technical Standards:
- OASIS Standard: MQTT Version 5.0 Specification
- OASIS Standard: MQTT Version 3.1.1 Specification
- Andy Stanford-Clark & Arlen Nipper: The Origin and History of MQTT
- HiveMQ Architecture Guides: MQTT Essentials & QoS Internals
- EMQX Distributed Broker Documentation: In-Memory Topic Tries & Shared Subscriptions






Top comments (1)
Great explanation! The distinction between pub/sub and traditional message queues is subtle but important, especially when designing IoT systems.