MQTT Fundamentals

How Does MQTT Work? Brokers, Clients and Messages Explained

A step-by-step explanation of how MQTT works, from opening a TCP connection to CONNECT, SUBSCRIBE, PUBLISH, keep alive and disconnect, with a trace you can reproduce on a public broker.

How does MQTT work diagram showing clients sending CONNECT, SUBSCRIBE and PUBLISH packets through a central broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

How does MQTT work? In short, clients open one long-lived connection to a broker, declare which topics they care about, and the broker forwards every published message to the matching subscribers. In this article you will follow that process packet by packet, from TCP connection through CONNECT, SUBSCRIBE, PUBLISH, keep alive and disconnect, and reproduce the whole trace yourself on a public broker.

The Three Roles: Publisher, Subscriber and Broker

MQTT has only two kinds of participants: clients and brokers. A client can publish, subscribe or both. The broker sits in the middle, receives every message and decides who gets it.

  • Publishers send messages to a topic, a string such as plant1/line2/temperature. They do not know or care who receives them.
  • Subscribers tell the broker which topics, or topic patterns, they want. They do not know who sends.
  • The broker authenticates clients, keeps track of subscriptions and sessions, and routes each message to every matching subscriber.

This decoupling is the heart of the design. A sensor does not need the IP address of a dashboard, the two do not need to be online at the same moment if sessions are persistent, and adding a fifth consumer requires no change to the sensor. For more on the broker's job, see what an MQTT broker does, and for the pattern itself, the MQTT publish/subscribe model.

Step 1: Open a Network Connection

MQTT runs over a reliable, ordered byte stream, normally TCP. The client opens a TCP connection to the broker, on port 1883 for plain MQTT or 8883 for MQTT over TLS. Browsers, which cannot open raw TCP sockets, carry MQTT inside a WebSocket instead (on ConnectMQ, 8083 or 8084 with path /mqtt). Nothing MQTT-specific has happened yet; this is just the pipe.

With TLS, a handshake now happens: the client checks the broker's certificate and both sides agree on encryption keys. Every MQTT packet afterwards is encrypted.

Step 2: CONNECT and CONNACK

The client's first packet must be CONNECT. It carries:

Field Purpose
Protocol name and level MQTT and 4 (3.1.1) or 5 (5.0)
Client ID Identifies the session; must be unique per connection
Clean session / clean start Start fresh, or resume a stored session
Keep alive Maximum seconds between packets from the client
Username and password Optional credentials
Last Will Optional message the broker publishes if the client vanishes
Properties (MQTT 5) Session expiry, receive maximum, maximum packet size and more

The broker validates all of this and answers with CONNACK. In MQTT 5 it contains a reason code: 0x00 for success, or an error such as bad credentials, not authorized or, on a quota-limited broker, 0x97 quota exceeded. It also says whether a previous session was resumed (session present flag) and can tell the client about server limits. Until a successful CONNACK arrives, the client must not send anything else.

Step 3: SUBSCRIBE and SUBACK

A client that wants messages sends SUBSCRIBE with one or more topic filters and a requested QoS for each. Filters can include wildcards: + matches exactly one level and # matches everything below a level. For example, plant1/+/temperature matches plant1/line2/temperature but not plant1/line2/motor/temperature.

The broker replies with SUBACK, listing the granted QoS (or a failure code) per filter. The granted QoS may be lower than requested if the broker caps it. If retained messages exist on matching topics, the broker sends them right away, which is how a new dashboard learns the current state instantly.

Step 4: PUBLISH and the QoS Handshakes

To send data, a client sends PUBLISH with a topic name, a payload (any bytes, often JSON), a QoS level and a retain flag. What happens next depends on QoS:

  1. QoS 0, at most once. PUBLISH and done. No acknowledgement; if the connection drops at the wrong moment, the message is lost.
  2. QoS 1, at least once. The receiver answers with PUBACK. If the sender does not get it, it resends with the duplicate flag set, so the receiver may see the message twice.
  3. QoS 2, exactly once. A four-step exchange: PUBLISH, PUBREC, PUBREL, PUBCOMP. Slower, but no duplicates.

These handshakes happen hop by hop: once between the publisher and the broker, and separately between the broker and each subscriber. The broker then forwards the message to every client whose filter matches, using the lower of the publish QoS and the subscription's granted QoS. The full trade-offs are in MQTT QoS levels explained.

With the retain flag set, the broker also stores the message as the last known value for that topic and hands it to future subscribers.

Step 5: Keep Alive with PINGREQ and PINGRESP

While idle, a client must still prove it is alive within the keep alive interval. If it has nothing to publish, it sends PINGREQ and the broker answers PINGRESP. If the broker hears nothing for about one and a half times the keep alive period, it considers the client dead, closes the connection and publishes the client's Last Will, if one was set. This is how dashboards learn that a device went offline without anyone polling.

Step 6: DISCONNECT and Sessions

A clean shutdown sends DISCONNECT, which tells the broker not to publish the Last Will. What survives depends on the session settings: with clean start and no session expiry, the broker forgets the client's subscriptions. With a persistent session (MQTT 5 session expiry greater than zero), the broker keeps subscriptions and queues QoS 1 and 2 messages until the client returns or the expiry elapses.

Reproduce the Trace Yourself

You can watch the whole lifecycle on the free public MQTT broker at broker.connectmq.com. Mosquitto's clients print every packet with -d. Pick a unique random ID first; here it is q7m3x9.

# Terminal 1: subscriber with debug output
mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-q7m3x9 -V mqttv5 -d \
  -t 'connectmq/demo/q7m3x9/#' -q 1

# Terminal 2: publisher with debug output
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-q7m3x9 -V mqttv5 -d \
  -t 'connectmq/demo/q7m3x9/sensor/temp' -m '{"c":21.7}' -q 1

The subscriber's output looks like this (details vary by version):

Client sub-q7m3x9 sending CONNECT
Client sub-q7m3x9 received CONNACK (0)
Client sub-q7m3x9 sending SUBSCRIBE (Mid: 1, Topic: connectmq/demo/q7m3x9/#, QoS: 1, Options: 0x00)
Client sub-q7m3x9 received SUBACK
Subscribed (mid: 1): 1
Client sub-q7m3x9 received PUBLISH (d0, q1, r0, m1, 'connectmq/demo/q7m3x9/sensor/temp', ... (10 bytes))
Client sub-q7m3x9 sending PUBACK (m1, rc0)
{"c":21.7}
Client sub-q7m3x9 sending PINGREQ
Client sub-q7m3x9 received PINGRESP

And the publisher's:

Client pub-q7m3x9 sending CONNECT
Client pub-q7m3x9 received CONNACK (0)
Client pub-q7m3x9 sending PUBLISH (d0, q1, r0, m1, 'connectmq/demo/q7m3x9/sensor/temp', ... (10 bytes))
Client pub-q7m3x9 received PUBACK (Mid: 1, RC:0)
Client pub-q7m3x9 sending DISCONNECT

Read the PUBLISH line flags: d0 (not a duplicate), q1 (QoS 1), r0 (not retained), m1 (packet identifier 1). Try -q 2 and you will see PUBREC, PUBREL and PUBCOMP; add -r to the publisher, restart the subscriber and the retained message arrives immediately after SUBACK. The public area is readable by anyone, so only send test data.

The Same Flow in Python

Libraries hide the packets behind callbacks, but each one maps to a step above.

import time
import uuid

import paho.mqtt.client as mqtt

PREFIX = f"connectmq/demo/{uuid.uuid4().hex[:6]}"


def on_connect(client, userdata, flags, reason_code, properties):   # CONNACK
    print("CONNACK:", reason_code)
    client.subscribe(f"{PREFIX}/#", qos=1)                           # SUBSCRIBE


def on_subscribe(client, userdata, mid, reason_codes, properties):  # SUBACK
    print("SUBACK:", [str(rc) for rc in reason_codes])
    client.publish(f"{PREFIX}/sensor/temp", '{"c":21.7}', qos=1)     # PUBLISH


def on_publish(client, userdata, mid, reason_code, properties):     # PUBACK
    print("PUBACK for mid", mid)


def on_message(client, userdata, msg):                               # incoming PUBLISH
    print("Message:", msg.topic, msg.payload.decode())


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id=f"trace-{uuid.uuid4().hex[:8]}",
                     protocol=mqtt.MQTTv5)
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_publish = on_publish
client.on_message = on_message

client.connect("broker.connectmq.com", 1883, keepalive=30)          # TCP + CONNECT
client.loop_start()
time.sleep(5)
client.disconnect()                                                  # DISCONNECT
client.loop_stop()

Common Misunderstandings

  • "The broker stores all messages." Only retained messages (one per topic) and queued messages for persistent sessions. MQTT is not a database or a log.
  • "Topics must be created first." No, topics exist the moment someone publishes or subscribes.
  • "QoS 2 end to end." QoS applies per hop and is downgraded to the subscription's level.
  • "Client IDs can be shared." A second connection with the same ID kicks the first one off.
  • "MQTT uses UDP." Standard MQTT needs TCP or another reliable stream; MQTT-SN is the variant for UDP and similar networks.

To go further, the MQTT tutorial builds a complete project, MQTT control packets details every packet type, and the official specifications are published at mqtt.org.

Try MQTT on ConnectMQ

Experiment freely on broker.connectmq.com, then create a free Serverless broker at app.connectmq.com with no credit card when you want private, authenticated topics. ConnectMQ is a cloud MQTT broker supporting MQTT 3.1.1 and 5.0 with an in-browser client and live topic explorer, so you can watch the flows described here in real time. When you move to production, it runs as a fully managed MQTT service.

Frequently Asked Questions

How does MQTT work in simple terms?

Devices connect to a central broker, subscribe to topics they are interested in and publish messages to topics. The broker forwards every message to all matching subscribers.

Do publishers and subscribers connect directly to each other?

No. Every client connects only to the broker, which is why devices never need each other's addresses.

What happens if a subscriber is offline when a message is published?

With a persistent session and QoS 1 or 2, the broker queues the message until the subscriber reconnects or the session expires. Otherwise the message is not delivered to that client, although a retained message will still give it the latest value.

What port does MQTT use?

1883 for plain TCP and 8883 for TLS. WebSocket ports vary by broker; ConnectMQ uses 8083 and 8084 with path /mqtt.

Related articles

Why Your MQTT Client Keeps Disconnecting (and How to Fix It)

Reconnect loops are one of the most common MQTT problems. Learn the five usual causes, from duplicate client IDs to keep alive timeouts and oversized packets, and follow a diagnosis flow that pinpoints yours.

8 min read

Who Invented MQTT? The History from Oil Pipelines to IoT

MQTT was invented in 1999 by Andy Stanford-Clark of IBM and Arlen Nipper of Arcom to monitor oil pipelines over satellite. Follow its path to open source, OASIS, ISO and today's managed cloud brokers.

7 min read

MQTT Maximum Message Size: Limits and Large Payload Strategies

MQTT can technically carry a 256 MB packet, but brokers, client libraries and devices impose far lower limits. Learn where the real limits are, what happens when you exceed them, and how to send large payloads.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.