Chapter 4

The MQTT mental model

The MQTT mental model: broker/client, topic and the + # wildcards, QoS 0-1-2, retained, will, keepalive, clean session and shared subscription — abstract ideas grounded in a smart-home setting.

Why this matters

Connecting Mosquitto to Home Assistant is easy enough that you can run on "it works, that's enough" for years without ever looking underneath. But the moment you have to troubleshoot — why a device is fine one day and flaky the next, why the state after a reboot is not the current one, why duplicate messages turn up — you have to be able to read a handful of core MQTT terms: topic, QoS, retained, will, session.

This chapter is a mental model rather than an operating manual: it uses smart-home examples to turn each term into an idea you can reason through in your head. From here the same words keep coming back — Chapter 5, First connection, where you verify them by hand, Chapter 8 on Listeners, and Chapter 13 on the Home Assistant integration.

Core concepts

At the heart of MQTT is the publish/subscribe model. It splits the work into three roles: a publisher sends messages out under a topic; the broker routes and filters every message; a subscriber subscribes only to the topics it cares about.

The point is that a publisher and a subscriber never need to know the other exists; the only contract they share is the topic. When the broker — the EMQX you are running — receives a message on a topic, it hands that data to every client currently subscribed to that topic at the same time. That is why adding a device, or adding a screen, disturbs none of the endpoints already there.

Every client is an endpoint: a device, a phone app and Home Assistant are all clients. The parameters a client can bring with it when it connects that bear on delivery quality — keepalive, for one, and how long a session is held — are covered later in this chapter.

Terms at a glance

TermPlain EnglishIn one line
Publish / Subscribesending and receiving by topicThe model in which the broker sits in the middle and exchanges messages
Topicmessage labelThe hierarchical name a message is filed under
QoSquality of serviceThe three-level setting for how reliably a message is delivered
Retainedthe last message keptThe broker keeps the latest message on each topic
Willthe notice sent on your behalfWhat the broker publishes for a client when it disconnects
Keepalivethe heartbeatA regular heartbeat that stops you being counted as dropped
Clean Sessionstart fresh every timeThe setting that clears the session as soon as the connection ends

Hands-on

  1. Open the Dashboard's WebSocket test client

    In the EMQX Dashboard, find the built-in visual WebSocket client (the Diagnose section / Diagnose tools). It connects to your broker as MQTT over WebSocket.

  2. Subscribe to a topic

    Add a subscriber and subscribe to a topic such as living-room/humidity. Do not attach anything practical to it yet; the point is to get a direct impression of subscribe → receive.

  3. Publish a message

    Switch to the publisher role, type living-room/humidity as the topic and a number as the payload, then send it and watch whether the subscriber receives it straight away.

  4. Try QoS and retained

    Change the publish QoS to 1, tick "Retain", and get a feel for how retained behaves: a subscriber that arrives later still receives the message immediately. Chapter 5 has the full demonstration; here you are only building the intuition.

Topic structure and wildcards

A topic is a UTF-8 string, split into levels by a forward slash /. In living-room/temperature, for example, temperature is the level under the living room. A publisher sends its messages to a specific topic; a subscriber can use a wildcard to subscribe to many topics at once.

MQTT gives you two wildcards:

  • + (single level): matches exactly one level. living-room/+/temperature, for example, receives living-room/east/temperature and living-room/west/temperature, but does not receive living-room/east/upper/temperature.
  • # (multi level): matches any number of levels, and must come last. living-room/#, for example, receives living-room, living-room/temperature and living-room/east/upper/temperature.

The rules matter: + and # can be used only to subscribe, never to publish; and each one either fills a whole level or, in the case of #, comes last.

QoS (quality of service) is a reliability setting carried by each message on its own, and it has three levels:

QoSGuaranteeScenario
0At most once, may be lostTelemetry such as temperature and humidity, where losing one reading does no harm
1At least once; duplicates are possibleSwitch commands, though a duplicate copy may arrive
2Exactly once, never duplicatedPayment and accounting logic, where a duplicate is never acceptable

The higher the QoS, the more negotiation and transfer it costs. QoS 0 is a good deal for everyday telemetry; keep 1 and 2 for devices whose control has to be guaranteed.

Session, retained, will and shared subscriptions

Session is the state a client and the broker keep between them, and it is what makes QoS 1 and 2 work correctly. MQTT 5.0 controls a session with clean start and the session expiry interval; MQTT 3.1.1 uses the clean session flag instead. With a clean session, the session is discarded the moment the client disconnects; if you want the QoS 1 and 2 messages from the offline period delivered after the client reconnects, use a persistent session.

Keepalive is how long the client promises to go between control packets sent to the broker; if the broker hears nothing for longer than that, it treats the connection as dropped. When you adjust how long counts as dropped, keepalive is what you are adjusting.

Retained message: mark a message as Retain and the broker stores the latest one for that topic. Any new subscriber to that topic receives it immediately, without waiting for the publisher to publish again. The cost is that only the latest message per topic is kept; to clear it, publish an empty message to that topic.

Will message: the client sets a will (a topic plus a payload) when it connects, and when the client drops unexpectedly the broker publishes that will to the subscribers on its behalf, so everyone else knows straight away that its state has changed.

A shared subscription takes the form $share/<group>/<topic> and spreads the message load evenly across the subscribers in one group (round_robin by default), instead of giving every one of them a duplicate copy. To raise throughput or add redundancy, split the work across several subscribers so each handles its own share.

Troubleshooting

  • You subscribed to a wildcard topic but nothing arrives: check first that +/# are used only on a subscription and sit in a legal position; sensor/#/temp, for example, puts # in the wrong place and is not valid.
  • Messages look like they arrive twice: QoS 1 is by nature at least once, which means duplicates are possible. If duplicates are unacceptable, move to QoS 2, or make the receiving end idempotent.
  • No earlier state after a restart: retained was not set. Mark the state message as Retain, or switch to a persistent session so the QoS 1 and 2 data from the offline period is delivered afterwards.
  • The session is not kept after a disconnect: only QoS 1 and 2 messages on a session that is being kept are queued while the client is offline; plain QoS 0 is not kept at all. If you want data from the offline period delivered later, use a persistent session rather than a clean session.

FAQ

Does MQTT always need a broker?

Yes. MQTT is a protocol built on a broker forwarding messages; without one there is no third party between two endpoints to route anything, and it is no longer MQTT. Your EMQX is that broker.

How does retained differ from an ordinary message?

An ordinary message goes only to whoever is subscribed at that moment; a retained message is kept by the broker, so any client that subscribes to the same topic afterwards also receives it immediately. Only the latest message per topic is kept.

Can will and retained be used together?

Yes. When you set the will you can tick "Retain" as well, so that besides publishing the will on an unexpected disconnect, the broker also stores it as a retained message, and anyone subscribing later gets the state right away.

With a shared subscription, who receives each message?

Within one $share/<group>/ group, each message is received by exactly one client in the group (chosen by the strategy in force); separate groups each receive their own copy. That makes it a good fit for spreading load.

Official sources