MQTT Publish, Subscribe & Unsubscribe: Packets Explained

By | May 24, 2026

Once a client has connected to a broker, almost everything it does comes down to three operations: publishing messages, subscribing to topics, and unsubscribing from them. Each operation has its own control packet, and most are confirmed by an acknowledgement packet from the broker. Understanding the fields these packets carry, and the acknowledgements they trigger, is what lets you reason precisely about how data moves through an MQTT system.

This article walks through all three operations in detail: the PUBLISH packet and what happens to a message after it reaches the broker, the SUBSCRIBE packet and its SUBACK acknowledgement, and the UNSUBSCRIBE packet and its UNSUBACK acknowledgement. It is a technical reference; related topics such as topic structure, Quality of Service, and the connection handshake each have their own dedicated articles in this category.

Operations at a glance

OperationPacket sentAcknowledged byDirection
PublishPUBLISHPUBACK (QoS 1), PUBREC/PUBREL/PUBCOMP (QoS 2), none (QoS 0)Either way
SubscribeSUBSCRIBESUBACKClient → Broker
UnsubscribeUNSUBSCRIBEUNSUBACKClient → Broker

Publishing a message

A client can publish messages as soon as it connects to a broker; there is no separate step to “open” a topic first. MQTT uses topic-based filtering, so every message must carry a topic that the broker can use to forward it to interested clients. Alongside the topic, a message typically carries a payload, which contains the actual data to transmit, in byte format.

A defining property of MQTT is that it is data-agnostic: the protocol does not care what is in the payload. The use case of the client determines how the payload is structured. The publishing client decides entirely on its own whether to send binary data, text in any encoding, a full XML or JSON document, an image, encrypted bytes, or anything else. The broker treats the payload as an opaque sequence of bytes and never needs to look inside it, which is part of why subject-based filtering on the topic, rather than the content, is the right fit.

Fields of the PUBLISH packet

A PUBLISH message has several attributes worth examining individually.

Topic Name. The topic name is a simple string, structured hierarchically with forward slashes as delimiters. Examples include myhome/livingroom/temperature or Germany/Munich/Oktoberfest/people. The topic is what the broker matches against subscriptions to decide who receives the message. Topic structure, wildcards, and naming conventions are covered in full in the topics article.

QoS. This number indicates the Quality of Service level of the message: 0, 1, or 2. The service level determines what kind of delivery guarantee the message has for reaching its intended recipient, whether that recipient is the broker (on the publish hop) or a subscribing client (on the delivery hop). The three levels and their tradeoffs are covered in the Quality of Service article.

Retain Flag. This flag defines whether the broker stores the message as the last retained value for the topic. When the retain flag is set, the broker stores the message, and any client that later subscribes to a matching topic immediately receives that retained message. This is how a new subscriber can learn the current state of a topic without waiting for the next live publish. The mechanism is covered in the retained messages article.

Payload. This is the actual content of the message. Because MQTT is data-agnostic, the payload can be images, text in any encoding, encrypted data, or virtually any data represented in binary.

Packet Identifier. The packet identifier uniquely identifies an in-flight MQTT transaction between the client and the broker, rather than the application message itself; the same payload sent again later may carry a different identifier. It is only relevant for QoS levels greater than zero, since QoS 0 needs no acknowledgement to match against. The client library or the broker is responsible for setting this internal identifier; application code generally never sees it.

DUP Flag. This flag indicates that the PUBLISH packet might be a re-delivery attempt. The specification deliberately uses “might,” because after an uncertain network state a sender cannot always know whether its first transmission actually reached the recipient, so it may resend out of caution. It is only relevant for QoS greater than 0. The resend mechanism is normally handled by the MQTT client library or the broker as an implementation detail, not something application code manages directly.

What happens after a client publishes

When a client sends a PUBLISH to the broker, the broker validates and processes the message according to its QoS handling rules, acknowledging it as that QoS level requires. Exactly when the acknowledgement happens relative to processing is implementation-specific: some brokers persist the message before acknowledging, some route it to subscribers before acknowledging, and some acknowledge before downstream delivery. Processing itself means determining which clients are subscribed to the topic and sending the message to each of them.

There is an important asymmetry here that follows directly from the publish/subscribe model. The client that publishes the message is only concerned with delivering the PUBLISH to the broker. Once the broker has received the PUBLISH, it becomes the broker’s responsibility to deliver the message onward to all subscribers. The publishing client receives no feedback about whether anyone was interested in the message, or about how many clients ultimately received it. From the publisher’s point of view, the job is done the moment the broker acknowledges receipt.

This is not a limitation so much as a design choice that keeps publishers simple and decoupled from subscribers. If your application genuinely needs to know that a specific recipient received and acted on a message, that confirmation is built on top of the protocol, for example with MQTT 5’s request/response pattern, covered in its own article.

The publish acknowledgement flows

What the broker sends back after a PUBLISH depends entirely on the QoS level of that message, and the three levels produce three very different exchanges. Although the full reasoning behind each level lives in the Quality of Service article, it is worth seeing the packet flows here, because they are the acknowledgements the publish operation actually generates.

At QoS 0, there is no acknowledgement at all. The client sends a single PUBLISH and the operation is complete. Nothing comes back. This is the “fire and forget” level, and it is the only publish that produces no return packet.

At QoS 1, the exchange is a single round trip:

Client  --- PUBLISH (QoS 1, packet id) -->  Broker
Client  <-- PUBACK  (same packet id)    ---  Broker

The sender stores the message until it receives a PUBACK carrying the matching packet identifier. If no PUBACK arrives, the sender resends the PUBLISH with the DUP flag set, and keeps doing so until it is acknowledged. The MQTT specification does not define the retransmission timing; how long a sender waits before resending is implementation-specific. Because a resend can happen after the original was in fact received, the message may be delivered more than once, which is why QoS 1 is “at least once.”

At QoS 2, the exchange is a four-part handshake that guarantees exactly-once delivery between MQTT peers at the protocol level. (Application-level duplicates can still arise from session persistence, client implementation bugs, bridge behavior, or backend processing logic; the protocol guarantee covers the MQTT exchange itself, not your application’s handling of the result.)

Client  --- PUBLISH  (QoS 2, packet id) -->  Broker
Client  <-- PUBREC   (same packet id)   ---  Broker
Client  --- PUBREL   (same packet id)   -->  Broker
Client  <-- PUBCOMP  (same packet id)   ---  Broker

All four packets share the same packet identifier, which is how the two sides coordinate. The handshake ensures the message is neither lost nor duplicated, at the cost of two full round trips. The detailed semantics of each step, and why this prevents duplication, are covered in the Quality of Service article.

Note that these flows are symmetric: the same exchanges occur on the broker-to-subscriber hop when the broker delivers a message, with the broker acting as sender. The packet identifier in that delivery is the broker’s own, not the publisher’s, because identifiers are scoped to a single client-broker interaction.

Anatomy of a PUBLISH packet

To make the fields concrete, consider a PUBLISH carrying a temperature reading. On the wire its first byte encodes the packet type and the publish-specific flags. For a message published at QoS 1 with the retain flag set and not a duplicate, the first byte is 0b00110011 (0x33): the high nibble 0011 is the PUBLISH type, bit 3 (DUP) is 0, bits 2 to 1 (QoS) are 01 for QoS 1, and bit 0 (RETAIN) is 1. The bytes that follow encode the remaining length, then the topic name (home/livingroom/temperature), then the two-byte packet identifier (present because QoS is greater than 0), and finally the payload (21.5). A QoS 0 publish of the same message would omit the packet identifier entirely, since none is needed. This is the level of compactness that makes MQTT efficient: the entire control overhead for a small reading is a handful of bytes.

Subscribing to topics

Publishing a message accomplishes nothing if no one ever receives it. For a message to reach a client, that client must have subscribed to the topic. To receive messages on the topics it cares about, a client sends a SUBSCRIBE message to the broker. The SUBSCRIBE message is straightforward; it contains a packet identifier and a list of subscriptions.

Fields of the SUBSCRIBE packet

Packet Identifier. As with PUBLISH, the packet identifier uniquely identifies the SUBSCRIBE message as it flows between client and broker, so the corresponding SUBACK can be matched to it. The client library or the broker sets this internal identifier.

List of Subscriptions. A single SUBSCRIBE message can contain multiple subscriptions for a client; you do not need one SUBSCRIBE per topic. Each subscription in the list is made up of a topic and a QoS level. The topic in a subscribe message can contain wildcards, which makes it possible to subscribe to a whole pattern of topics rather than one specific topic. This is what allows a single subscription to home/+/temperature or home/# to match many topics at once, as covered in the topics article.

One subtlety is worth knowing: if a client has overlapping subscriptions that both match the same topic, most brokers deliver a single copy using the highest matching QoS level among those subscriptions. However, overlapping-subscription behavior historically varied between implementations in MQTT 3.1.1, where the specification left room for broker-specific handling, so some brokers may deliver more than one copy. MQTT 5 clarified this behavior. If you rely on receiving exactly one copy from overlapping subscriptions, verify how your specific broker handles it.

The SUBACK acknowledgement

To confirm each subscription, the broker sends a SUBACK acknowledgement message back to the client. The SUBACK contains the packet identifier of the original SUBSCRIBE message, so the client can clearly identify which request it answers, and a list of return codes.

Packet Identifier. This is the same packet identifier that was in the SUBSCRIBE message, used to tie the acknowledgement to the request.

Return Codes. The broker sends one return code for each topic/QoS pair it received in the SUBSCRIBE message. If the SUBSCRIBE contained five subscriptions, the SUBACK contains five return codes, in the same order. Each return code acknowledges the corresponding topic and reports the QoS level that the broker actually granted. If the broker refuses a particular subscription, the SUBACK contains a failure return code for that specific topic, for example because the client has insufficient permission to subscribe to it, or because the topic is malformed.

The MQTT 3.1.1 SUBACK return codes:

Return codeMeaning
0Success — Maximum QoS 0
1Success — Maximum QoS 1
2Success — Maximum QoS 2
128Failure

MQTT 5 expands the SUBACK with detailed reason codes that go well beyond these simple success and failure values, distinguishing specific causes such as a topic filter being invalid, the client being unauthorized, or shared subscriptions not being supported. Since modern brokers increasingly default to MQTT 5, expect richer SUBACK information there; the improved client feedback article covers this.

The “Maximum QoS” wording in the success codes is important. It reflects that the broker may grant a lower QoS than the client requested. If a client asks to subscribe at QoS 2 but the broker, by policy or configuration, only grants QoS 1, the SUBACK returns code 1, and the broker will deliver messages on that topic to the client at QoS 1. Always read the granted QoS from the SUBACK rather than assuming you got what you asked for.

After a client successfully sends a SUBSCRIBE and receives the SUBACK, it begins receiving every published message that matches a topic in the subscriptions the SUBSCRIBE contained. From that point, the broker pushes matching messages to the client as they arrive.

Unsubscribing from topics

The counterpart to SUBSCRIBE is UNSUBSCRIBE, which deletes a client’s existing subscriptions on the broker. The UNSUBSCRIBE message is similar in shape to the SUBSCRIBE message: it has a packet identifier and a list of topics.

Fields of the UNSUBSCRIBE packet

Packet Identifier. The packet identifier uniquely identifies the UNSUBSCRIBE message as it flows between client and broker, so the UNSUBACK can be matched to it. As with the other packets, the client library or the broker sets this internal identifier.

List of Topics. The list of topics can contain multiple topics from which the client wants to unsubscribe in a single message. A useful detail: when unsubscribing, you only need to send the topic, not the QoS. The broker unsubscribes the client from the topic regardless of the QoS level with which the subscription was originally made. You do not have to remember or match the original QoS to remove a subscription.

The UNSUBACK acknowledgement

To confirm the unsubscribe, the broker sends an UNSUBACK acknowledgement message back to the client. In MQTT 3.1.1 this message contains only the packet identifier of the original UNSUBSCRIBE message, used to clearly identify which request it answers.

Packet Identifier. This is the same packet identifier that was in the UNSUBSCRIBE message.

After receiving the UNSUBACK from the broker, the client can assume that the subscriptions named in its UNSUBSCRIBE message are deleted, and it will no longer receive messages on those topics.

It is worth noting one MQTT 3.1.1 limitation here: because the UNSUBACK carries no payload beyond the packet identifier, the client cannot learn whether, or why, an unsubscribe failed; it simply receives confirmation that the request was processed. MQTT 5 changes this by adding reason codes to the UNSUBACK, so a client can be told, for instance, that no such subscription existed or that it was not authorized to unsubscribe. That improvement is covered in the improved client feedback article.

Subscriptions, reconnections, and persistence

Subscriptions are not necessarily permanent, and what happens to them across a disconnect depends on the kind of session the client established when it connected. This directly affects whether a client must re-issue its SUBSCRIBE messages after reconnecting, so it is worth understanding alongside the operations themselves.

When a client connects with a clean (non-persistent) session, the broker keeps no state for it. If that client disconnects, its subscriptions are discarded. On reconnect, it starts from nothing and must send its SUBSCRIBE messages again to begin receiving messages. For a constrained device that loses its link frequently, re-subscribing on every reconnect is real overhead, both in bytes on the wire and in round trips before the device is fully operational again. (Note that MQTT 5 replaces the clean session flag with a clean start flag plus a session expiry interval, refining this model; the terminology here is MQTT 3.1.1’s, and the change is covered in the persistent sessions and MQTT 5 articles.)

When a client connects with a persistent session, the broker stores its subscriptions as part of the session state, keyed to the ClientId. If the client disconnects and later reconnects with the same ClientId and a non-clean session, the broker still holds those subscriptions, and the client does not need to re-subscribe. The session present flag in the CONNACK is how the client learns which situation it is in: a true value means the broker still has its subscriptions, a false value means it must subscribe again. This interaction is covered fully in the persistent sessions article, and the connection handshake article explains the session present flag in context.

Persistence also affects message delivery, not just the subscriptions themselves. For a client with a persistent session that subscribed at QoS 1 or 2, the broker queues matching messages published while the client is offline and delivers them on reconnect. A client with a clean session, or one subscribed at QoS 0, receives nothing it missed while away. So the choice between clean and persistent sessions, made at connect time, shapes the behavior of every subscribe and publish operation that follows.

Practical subscription management

A few patterns recur when managing subscriptions in real systems, and they follow directly from how the operations work.

Group related subscriptions into a single SUBSCRIBE where you can. Because one SUBSCRIBE carries a list of topic/QoS pairs, subscribing to ten related topics in one message is cheaper than ten separate messages, each with its own round trip and SUBACK. The single SUBACK returns one return code per topic, in order, so you still learn the result for each.

Always read the granted QoS from the SUBACK rather than assuming the broker honored your request. A broker may grant a lower maximum QoS than you asked for, and the SUBACK is the authoritative record of what you actually got. Code 128 for any entry means that particular subscription was refused, and the client should treat that topic as not subscribed.

Prefer wildcards over long lists of near-identical topics when the topic hierarchy supports it. A single subscription to home/+/temperature is more maintainable than separately subscribing to the temperature topic of every room, and it automatically covers rooms added later. Be deliberate, though: subscribing to a broad wildcard such as # pulls in everything and is an anti-pattern in high-throughput systems, as the topics article explains.

Unsubscribe when a client no longer needs a topic, rather than leaving stale subscriptions in place. On a clean session this matters less, since the subscriptions vanish at disconnect, but on a persistent session, subscriptions and any queued QoS 1 or 2 messages for them survive across disconnects and consume broker resources until explicitly removed.

Finally, be aware that MQTT 5 adds a special form of subscription that changes the delivery model. A shared subscription, made with a topic of the form $share/<group>/<topic>, lets multiple client instances share one logical subscription so that each matching message goes to only one member of the group rather than to all of them. This turns the normal broadcast-to-all-subscribers behavior into load balancing across a group, which is useful for clustered consumers and for scaling out the ingestion of high-volume topics. Ordinary subscriptions still deliver a copy to every subscriber; shared subscriptions are the explicit opt-in when you want queue-like work distribution instead. The shared subscriptions article covers the syntax and delivery semantics in full.

The role of the packet identifier

A thread running through all three operations is the packet identifier, and it is worth pulling together what it does. The packet identifier is a two-byte value that uniquely identifies a packet as it flows between a specific client and the broker, so that a request can be matched to its acknowledgement. It appears in PUBLISH (for QoS greater than 0), in the acknowledgements of the publish flows, and in SUBSCRIBE, SUBACK, UNSUBSCRIBE, and UNSUBACK.

Two points matter in practice. First, the identifier is only meaningful within a single client-broker interaction; it is not unique across different clients, and once a given flow completes, the identifier becomes available for reuse. This is why the value never needs to exceed 65,535: it is unrealistic for a single client to have that many interactions in flight at once without completing any. Second, application developers generally do not manage packet identifiers themselves; the client library or the broker sets and tracks them as an internal detail. Understanding them is mostly useful for reasoning about how acknowledgements are matched and for interpreting packet captures when debugging.

Putting the operations together

A typical client session weaves these operations together. Consider a device that both reports data and accepts commands:

  1. After connecting, the device sends a SUBSCRIBE for its command topic, say devices/device1/command, at QoS 1, and waits for the SUBACK confirming the granted QoS.
  2. It then begins publishing its sensor readings to devices/device1/temperature with PUBLISH messages, choosing a QoS appropriate to how critical the data is.
  3. While it runs, the broker pushes any messages published to devices/device1/command down to the device, because of the subscription in step 1.
  4. If the device no longer needs commands, it sends an UNSUBSCRIBE for devices/device1/command and waits for the UNSUBACK before assuming the subscription is gone.

This interleaving of publishing and subscribing in the same client is the normal case, not the exception. The labels publisher and subscriber describe what the client is doing at each moment, not what kind of device it is.

How these operations relate to the rest of MQTT

The three operations covered here are the verbs of MQTT, and each connects to a larger topic:

  • The topic name in PUBLISH and the topic filters in SUBSCRIBE are governed by topic structure and wildcards, covered in the topics article.
  • The QoS field in PUBLISH and SUBSCRIBE, and the acknowledgement packets it triggers, are governed by the Quality of Service mechanism.
  • The retain flag in PUBLISH connects to retained messages.
  • The packet identifier ties these operations to the delivery guarantees that QoS 1 and 2 provide.
  • The expanded reason codes on SUBACK and UNSUBACK in MQTT 5 connect to that version’s improved client feedback.

Knowing exactly what each packet carries, and what acknowledgement it produces, is the foundation for reasoning about reliability and behavior in any MQTT deployment.

Frequently asked questions

Can an MQTT client publish before subscribing to anything?

Yes. A client can publish as soon as it is connected. Publishing and subscribing are independent operations, and a client that only publishes never needs to subscribe to anything.

Can a single SUBSCRIBE message subscribe to multiple topics?

Yes. A SUBSCRIBE message carries a list of subscriptions, each a topic plus a QoS level, so one message can subscribe to many topics at once. Each topic can also use wildcards to match a pattern.

What does the SUBACK return code tell me?

It reports, for each requested subscription, whether it succeeded and at what maximum QoS. Codes 0, 1, and 2 indicate success with that maximum QoS granted; code 128 indicates failure, for example due to insufficient permissions or a malformed topic. The granted QoS may be lower than requested, so read it from the SUBACK.

Does the publisher know if its message reached any subscribers?

No. The publisher’s responsibility ends when the broker acknowledges receipt of the PUBLISH. It gets no feedback about how many subscribers, if any, received the message. End-to-end confirmation requires MQTT 5’s request/response pattern.

Do I need to specify the QoS when unsubscribing?

No. An UNSUBSCRIBE carries only the topic, not the QoS. The broker removes the subscription regardless of the QoS level it was originally created with.

What is the packet identifier used for?

It uniquely identifies a packet between a specific client and the broker so that a request can be matched to its acknowledgement. It is used for QoS 1 and 2 publishes and for the subscribe and unsubscribe flows, is reusable once a flow completes, and is managed internally by the library or broker.

Author: Zakaria El Intissar

I'm an automation and industrial computing engineer with 12 years of experience in power system automation, SCADA communication protocols, and electrical protection. I build tools and write guides for Modbus, DNP3, IEC 101/103/104, and IEC 61850 on ScadaProtocols.com to help engineers decode, analyze, and troubleshoot real industrial communication systems.