MQTT in Plain Terms: Pub/Sub for Unreliable Networks
What MQTT is, how topics, QoS, retained messages, and Last Will work, and a minimal Mosquitto + JavaScript example to get a broker running locally.
Why MQTT exists
HTTP assumes a browser. A client asks, a server answers, done. That breaks down fast with a hundred battery sensors on bad Wi-Fi. They cannot poll every few seconds, they cannot pay a few hundred bytes of headers per reading, and they sleep half the day.
MQTT works the other way. Each device connects once to a broker and stays connected. A sensor publishes to a topic like home/living-room/temp. Anyone who subscribed gets the message pushed to them. The two sides never meet. The broker does the routing.
Headers start at 2 bytes. A keep-alive ping keeps the connection alive through NATs and sleep cycles, and the session can pick up where it left off after a drop.
Core pieces
1. Broker and clients
The broker is the hub. I use Mosquitto here. HiveMQ and NanoMQ fill the same role. Clients are whatever connects: an ESP32, a phone app, a Node script. One broker, lots of clients, no direct wiring between them.
Why not EMQX?
On paper EMQX wins most comparisons. Big clustering numbers, a built-in rule engine, MQTT over QUIC. I still skipped it.
EMQX started as open source around 2012 and was commercialized by EMQ, founded in 2017 as Hangzhou Yingyun Technologies in Hangzhou, China. The site now lists global HQ as Menlo Park, California.
That history was enough for me to pass. A broker sees every topic and payload in plaintext, so I care where the vendor sits. Mosquitto is smaller, EPL-licensed, and I can self-host it and read the whole config in an afternoon.
2. Topics
A topic is just a slash-separated string. Nothing to create first. Publish to sensors/kitchen/humidity and it exists. Subscribe to that same string and you get messages.
Two wildcards do most of the work:
| Wildcard | Meaning | Example |
|---|---|---|
+ | One level | sensors/+/humidity matches sensors/kitchen/humidity but not sensors/kitchen/a/humidity |
# | Rest of tree | sensors/# matches everything under sensors/ |
I keep topics lowercase and shaped like site/device/metric. What I stopped doing is putting volatile IDs or JSON paths in there. It makes ACLs painful later.
3. QoS: pick one delivery guarantee
| QoS | Name | Behavior | Use when |
|---|---|---|---|
| 0 | At most once | Fire and forget | High-rate sensor data where one lost reading doesn’t matter |
| 1 | At least once | Acked, may duplicate | Alerts and state changes, with idempotent handlers |
| 2 | Exactly once | Four-way handshake | Billing or commands where a duplicate hurts |
I default to 1. QoS 0 loses data silently, and QoS 2 costs two extra round trips per message. Save 2 for the rare message that must arrive once.
4. Retained messages
By default you only hear messages published after you subscribe. Set the retain flag and the broker holds onto the last message per topic and hands it to each new subscriber right away.
I use retain for state, like door/lock set to locked. I never use it for events like motion/detected. A retained event sits there and looks fresh to the next subscriber, which caused me a confusing afternoon once.
5. Last Will and Testament (LWT)
The will is a message the broker sends on your behalf if you vanish. You register it at connect time.
My usual pattern:
- Connect with the will
home/sensor-1/statusset tooffline, retained. - On startup, publish
onlineto the same topic. - If the battery dies or Wi-Fi drops, the broker publishes
offlinefor you.
No heartbeat code. Presence falls out of the protocol.
6. MQTT 5
MQTT 5 came out in 2019 and most brokers now speak both 3.1.1 and 5. It adds reason codes that tell you why something failed, shared subscriptions like $share/group/topic for splitting load across consumers, message expiry, user properties, and cleaner auth. Use 5 for anything new.
MQTT vs HTTP, briefly
| MQTT | HTTP | |
|---|---|---|
| Pattern | Pub/sub, pushed | Request/response, polled |
| Overhead per message | About 2 bytes plus topic | Headers, often hundreds of bytes |
| Direction | Both ways on one connection | Client starts it |
| Offline handling | Queued sessions, LWT, retained state | Nothing built in |
| Best for | Telemetry, commands, chatty devices | Documents, APIs, one-shot fetches |
One device fanning out to many, or many devices reporting in all day: MQTT. Caching, content negotiation, a public API: HTTP.
Run it locally in 5 minutes
Install Mosquitto:
# macOS
brew install mosquitto
mosquitto -v
# Ubuntu
# sudo apt install mosquitto mosquitto-clients
In one terminal, subscribe:
mosquitto_sub -h localhost -t "home/#" -v
In another, publish:
mosquitto_pub -h localhost -t "home/living-room/temp" -m "22.4" -q 1 --retain
The first terminal prints home/living-room/temp 22.4 right away. Kill it, resubscribe, and you get the same value again from the retained copy.
Same thing in JavaScript with mqtt.js:
npm install mqtt
import mqtt from "mqtt";
const client = mqtt.connect("mqtt://localhost:1883", {
clientId: "desk-monitor-1",
// will: { topic: "desk/monitor-1/status", payload: "offline", retain: true },
});
client.on("connect", () => {
client.subscribe("home/#", { qos: 1 });
client.publish("home/living-room/temp", "22.4", { qos: 1, retain: true });
});
client.on("message", (topic, payload) => {
console.log(topic, payload.toString());
});
Security checklist
Out of the box Mosquitto is plaintext on 1883 with no login. Fine on localhost. Before you expose it:
- Turn on username/password or client certificates.
- Put TLS on 8883. Let’s Encrypt is fine for a small box.
- Add ACLs so each device only touches its own subtree.
- Keep secrets out of topic names. Broker logs record topics in cleartext.
- Cap payload size and connection rate. One stuck loop publishing in a tight while loop will bury a broker.
When not to use it
- Large files. Send those over HTTP or S3 and pass the URL through MQTT.
- Ordering across topics. MQTT only orders within one topic at a given QoS.
- True end-to-end encryption. TLS covers you to the broker and then again from the broker out, but the broker itself sees plaintext. If that matters, encrypt the payload before you publish.
Further reading
Citation
For attribution, please cite this work as:
Shimizu, Kye (2026). MQTT in Plain Terms: Pub/Sub for Unreliable Networks. kyeshimizu.com. https://kyeshimizu.com/blog/2026-09-15-mqtt
@misc{shimizu20262026,
author = {Kye Shimizu},
title = {MQTT in Plain Terms: Pub/Sub for Unreliable Networks},
year = {2026},
url = {https://kyeshimizu.com/blog/2026-09-15-mqtt},
note = {kyeshimizu.com}
}