Tiny devices don't talk to each other directly — they publish to topics, and a broker fans each message out to whoever subscribed.
A temperature sensor doesn't know your phone exists, and your phone doesn't know the sensor exists. Instead, the sensor publishes a reading to a named topic like home/kitchen/temp, and anyone who subscribed to a matching topic filter receives a copy. A central broker sits in the middle and does the matching and the fan-out.
This decouples producers from consumers completely: they never need each other's address, and you can add a tenth subscriber without touching the sensor. That fit — small message headers, one persistent TCP connection, a server doing the routing — is exactly what makes MQTT suit constrained IoT devices.
Each client opens one long-lived TCP connection to the broker. A subscriber registers a topic filter (which may contain wildcards); a publisher sends a message to a concrete topic name (no wildcards allowed). The broker matches filters against the topic and forwards a copy to each match. Here is a minimal client with paho-mqtt in Python.
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, rc):
# '+' = exactly one level, '#' = the rest of the tree (must be last)
client.subscribe("home/+/temp", qos=1)
def on_message(client, userdata, msg):
print(msg.topic, msg.payload.decode()) # e.g. home/kitchen/temp 21.4
client = mqtt.Client(client_id="thermostat-1", clean_session=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.local", 1883, keepalive=60)
# Publishers send to a concrete topic, never a wildcard:
client.publish("home/kitchen/temp", payload="21.4", qos=1, retain=False)
client.loop_forever()
The broker never inspects the payload — to it the message is opaque bytes. All the routing happens on the topic string alone.
| Level | Guarantee | Round trips | Duplicates? |
|---|---|---|---|
| QoS 0 · at most once | Fire and forget — may be lost | PUBLISH only | No (or lost) |
| QoS 1 · at least once | Delivered, retried until acked | PUBLISH → PUBACK | Possible |
| QoS 2 · exactly once | Delivered once, no dupes | 4-step: PUBLISH → PUBREC → PUBREL → PUBCOMP | No |
Higher QoS costs more round trips, latency, and broker state. The broker is the single point of fan-out, so every message flows through it. A retained message is one the broker stores per topic and hands to any future subscriber the moment it subscribes — convenient for a "last known value", but it means a brand-new subscriber can receive a message that was published long before it connected.
# must be the last level, on its own. home/# is valid and matches the whole subtree; home/#/temp is illegal. + may appear at any level, even several times: home/+/+ is fine.+ is exactly one level — not zero, not many. home/+ matches home/door but not home/kitchen/temp (one level too deep) and not home (one level too shallow).retain=True to clear it.client_id — the broker disconnects the older one. And clean_session=False (persistent session) queues QoS 1/2 messages for an offline client; True drops them.A thermostat subscribes with the filter home/+/temp — "any room's temperature". The kitchen sensor then publishes 21.4 to the concrete topic home/kitchen/temp at QoS 1.
The broker walks the topic level by level against each subscriber's filter. For the thermostat: home=home matches, + swallows kitchen, temp=temp matches — hit. It also matches the phone on home/# (the # covers kitchen/temp) and the logger on home/kitchen/+ (the + swallows temp). A door-only app subscribed to home/door does not match. The broker sends one copy to each of the three matching subscribers, and because it's QoS 1, each returns a PUBACK so the broker knows the delivery landed.
A subscriber's filter is home/#. A sensor publishes to home/kitchen/temp. Does it match?
A subscriber's filter is home/+. A sensor publishes to home/kitchen/temp. Does it match?