MQTT subscription options are per-subscription flags introduced in MQTT 5.0 that control how the broker delivers messages to you: whether you receive your own publications, whether the retain flag is preserved, and whether retained messages are replayed when you subscribe. By the end of this article you will know what each option does, which real problems it solves and how to set it in JavaScript and Python.
Where Subscription Options Live
In MQTT 3.1.1 a SUBSCRIBE packet carries a topic filter and one byte that holds the requested QoS. MQTT 5.0 keeps that byte but uses the spare bits for new options. Each topic filter in a SUBSCRIBE gets its own options byte:
| Bits | Option | Values |
|---|---|---|
| 0–1 | Maximum QoS | 0, 1 or 2 |
| 2 | No Local | 0 = off, 1 = on |
| 3 | Retain As Published | 0 = off, 1 = on |
| 4–5 | Retain Handling | 0, 1 or 2 |
| 6–7 | Reserved | must be 0 |
Because the options are per filter, one client can subscribe to chat/room1 with No Local on and to config/# with different retain behaviour in the same packet. The full definitions are in the OASIS MQTT 5.0 specification. The options only exist when the client connects with protocol version 5; a 3.1.1 connection cannot express them. For context on the rest of the version, see the overview of MQTT 5 features.
Maximum QoS
This is the familiar QoS from MQTT 3.1.1, renamed to make its meaning clear. It is the highest QoS at which the broker will deliver messages on this subscription. A message published at QoS 2 to a subscription with Maximum QoS 1 arrives at QoS 1. A message published at QoS 0 is never upgraded. If the MQTT QoS levels are new to you, read that guide first, because the downgrade rule is where many delivery surprises come from.
No Local: Stop Receiving Your Own Messages
By default, if a client publishes to a topic it is also subscribed to, the broker sends the message back to it. With No Local set, the broker does not forward a message to the connection that published it.
Typical cases:
- Chat and collaboration apps. Every participant publishes to and subscribes to
chat/room1. Without No Local, each user sees their own message twice: once from the local UI and once echoed by the broker. - Two-way sync. A service that both publishes and listens on
devices/+/configwould otherwise process its own updates and might loop. - Bridges. A bridge that subscribes and republishes on the same broker can create message loops; No Local on the bridge side breaks them.
Two rules to remember. No Local works per connection, identified by client ID, so two different clients run by the same application still receive each other's messages. And it is a protocol error to set No Local on a shared subscription ($share/...), because "local" has no meaning for a group.
Retain As Published
When the broker forwards a live message to subscribers, by default it clears the RETAIN flag, even if the publisher set it. The flag is only set on messages that the broker sends because of a new subscription (the stored retained message). That default lets subscribers distinguish "this is the stored last value" from "this just happened".
With Retain As Published on, the broker keeps the RETAIN flag exactly as the publisher set it. You need this mainly when the subscriber republishes messages somewhere else and the retained status must survive:
- Bridges between brokers that mirror retained state.
- Gateways and proxies that forward to a second system which itself honours retained messages.
- Debugging tools that want to show what the publisher actually sent.
For ordinary application subscribers, leave it off.
Retain Handling: Control Retained Replays
Retained messages are sent to a client when it subscribes. Retain Handling decides when that happens:
| Value | Behaviour | Use it when |
|---|---|---|
| 0 | Send retained messages at every subscribe (default) | Dashboards and consumers that need current state |
| 1 | Send retained messages only if the subscription did not already exist | Clients with persistent sessions that resubscribe on reconnect |
| 2 | Never send retained messages for this subscription | Event processors that only care about new messages |
Value 1 solves a subtle problem. A device with a persistent session reconnects and resubscribes defensively. With value 0 it receives every retained message again, and if those are configuration messages it may re-apply them. With value 1 the broker only replays retained messages when the subscription is genuinely new.
Value 2 is useful for loggers and alerting services. If an alerting service subscribes to alarms/# and the broker holds retained alarms from hours ago, value 0 would make it fire old alerts every time it restarts.
Setting MQTT Subscription Options in Code
Both examples connect to the public test broker at broker.connectmq.com over TLS with MQTT 5. Use your own unique topic prefix; the public area is readable by anyone, so keep the data harmless.
MQTT.js (Node.js)
// npm install mqtt
const mqtt = require("mqtt");
const prefix = "connectmq/demo/c41e";
const client = mqtt.connect("mqtts://broker.connectmq.com:8883", {
clientId: "subopts-js-c41e",
protocolVersion: 5,
});
client.on("connect", () => {
// No Local: do not echo our own chat messages back to us
client.subscribe(`${prefix}/chat/room1`, { qos: 1, nl: true }, (err, granted) => {
if (err) return console.error(err);
console.log("chat granted:", granted);
client.publish(`${prefix}/chat/room1`, "hello from js", { qos: 1 });
});
// Retain Handling 2: ignore stored alarms, only new ones
client.subscribe(`${prefix}/alarms/#`, { qos: 1, rh: 2 });
// Retain As Published: keep the RETAIN flag as sent
client.subscribe(`${prefix}/config/#`, { qos: 1, rap: true, rh: 1 });
});
client.on("message", (topic, payload, packet) => {
console.log(`${topic} retain=${packet.retain} -> ${payload.toString()}`);
});
Run it and you will not see "hello from js" printed, because No Local suppresses the echo. Publish to the same chat topic from another client and the message appears.
Python paho-mqtt 2.x
import ssl
import paho.mqtt.client as mqtt
from paho.mqtt.subscribeoptions import SubscribeOptions
PREFIX = "connectmq/demo/c41e"
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print("Connect failed:", reason_code)
return
client.subscribe([
(f"{PREFIX}/chat/room1", SubscribeOptions(qos=1, noLocal=True)),
(f"{PREFIX}/alarms/#", SubscribeOptions(
qos=1, retainHandling=SubscribeOptions.RETAIN_DO_NOT_SEND)),
(f"{PREFIX}/config/#", SubscribeOptions(
qos=1, retainAsPublished=True,
retainHandling=SubscribeOptions.RETAIN_SEND_IF_NEW_SUB)),
])
def on_subscribe(client, userdata, mid, reason_code_list, properties):
print("SUBACK:", [str(rc) for rc in reason_code_list])
def on_message(client, userdata, msg):
print(f"{msg.topic} retain={msg.retain} -> {msg.payload.decode()}")
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id="subopts-py-c41e", protocol=mqtt.MQTTv5)
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_message = on_message
client.connect("broker.connectmq.com", 8883, keepalive=60)
client.loop_forever()
Verifying the Behaviour
A quick test plan you can run in a few minutes:
- Publish a retained message to
connectmq/demo/c41e/alarms/pump1from any client, for example the in-browser MQTT client in the ConnectMQ portal ormosquitto_pub -r. - Start the Python script. The alarms subscription uses Retain Handling 2, so the retained alarm is not delivered.
- Publish a new, non-retained alarm. It arrives immediately.
- Publish a retained message to
.../config/rate. Because Retain As Published is on, the script printsretain=Truefor the live message. - Restart the script without changing anything else and confirm the behaviour is repeatable.
Common Pitfalls
- Connecting with MQTT 3.1.1. Libraries silently ignore subscription options on a 3.1.1 connection. Always set
protocolVersion: 5orprotocol=mqtt.MQTTv5. - No Local on shared subscriptions. The broker treats this as a protocol error and closes the connection.
- Expecting Retain Handling 1 to work with clean sessions. If the session is new every time, every subscription is new, so value 1 behaves like value 0.
- Using Retain As Published in dashboards. The dashboard then cannot tell a stored value from a fresh one. Keep the default unless you forward messages.
- Ignoring the SUBACK. A reason code such as
0x80or0x87means the subscription was refused. Log the SUBACK reason codes.
Choosing Options by Role
| Client role | Max QoS | No Local | Retain As Published | Retain Handling |
|---|---|---|---|---|
| Dashboard | 1 | off | off | 0 |
| Chat participant | 1 | on | off | 0 or 2 |
| Alerting service | 1 | off | off | 2 |
| Device with persistent session | 1 | off | off | 1 |
| Bridge or forwarder | 2 | on | on | 0 |
These options are part of what makes ConnectMQ and other MQTT 5 brokers easier to build on than their 3.1.1 predecessors: behaviour that used to require application-level filtering is now a flag in the SUBSCRIBE packet. Pair them with good topic design from MQTT wildcards, and keep the MQTT tutorial handy if you are still learning the basics.
Get Started with ConnectMQ
Test subscription options against the public MQTT test broker, then move to a private managed MQTT broker with MQTT 5.0, TLS and generated credentials. Create a free Serverless broker at app.connectmq.com, no credit card required.
Frequently Asked Questions
Are MQTT subscription options available in MQTT 3.1.1?
No. MQTT 3.1.1 only carries the requested QoS. No Local, Retain As Published and Retain Handling require an MQTT 5.0 connection.
What is the default value of each subscription option?
No Local and Retain As Published default to off, and Retain Handling defaults to 0, which sends retained messages on every subscribe. That matches MQTT 3.1.1 behaviour.
Why do I still receive my own messages with No Local enabled?
Check that you are connected with MQTT 5 and that the publisher and subscriber are the same connection. A second client, even in the same process, is not local.
Can I change subscription options without unsubscribing?
Yes. Sending a new SUBSCRIBE for the same topic filter replaces the existing subscription's options. With Retain Handling 1, retained messages are not resent because the subscription already exists.



