MQTT wildcards let a single subscription match many topics: + matches exactly one topic level and # matches any number of levels at the end of a filter. In this guide you will learn the precise matching rules, the edge cases that trip people up, and a set of mosquitto_sub commands you can run against a public broker to see each filter in action.
Topics vs Topic Filters
Publishers always publish to a concrete topic name, such as home/kitchen/temperature. Subscribers subscribe with a topic filter, which may contain wildcards. The broker compares each incoming topic with every subscription's filter, level by level, and delivers the message to every match. That routing job is the core of what an MQTT broker does.
Wildcards are only valid in filters. Publishing to a topic that contains + or # is a protocol error, and most brokers will disconnect the client.
The Single-Level Wildcard: +
+ matches exactly one level, whatever its content. It must occupy an entire level: home/+/temperature is valid, home/kit+/temperature is not.
| Filter | Matches | Does not match |
|---|---|---|
home/+/temperature |
home/kitchen/temperature, home/garage/temperature |
home/temperature, home/kitchen/sensor1/temperature |
+/+/status |
site1/pump3/status |
site1/status, site1/hall/pump3/status |
home/+ |
home/kitchen |
home, home/kitchen/light |
+ |
home |
home/kitchen, /home |
Two details are worth remembering. First, + also matches an empty level, so home/+/temperature matches home//temperature. Second, a leading slash creates an empty first level, so +/home matches /home. Both are good reasons to follow sensible MQTT topic naming conventions and avoid empty levels altogether.
You can use several + in one filter. factory/+/+/temperature matches every temperature two levels below factory, which is ideal for dashboards that show one measurement across a fleet.
The Multi-Level Wildcard:
# matches any number of levels, including zero, and must be the last character of the filter, occupying a whole level.
| Filter | Matches | Does not match |
|---|---|---|
home/# |
home, home/kitchen, home/kitchen/light/state |
office/kitchen |
home/kitchen/# |
home/kitchen, home/kitchen/temperature |
home/garage/temperature |
# |
Every topic not starting with $ |
$SYS/... |
home/+/light/# |
home/kitchen/light, home/kitchen/light/brightness |
home/light |
Invalid filters include home/#/temperature (# not last), home# (not a whole level) and home/kitchen#.
Note that home/# also matches the parent topic home itself. If you only want children, use home/+/#, which requires at least one level below home.
Wildcards and $ Topics
Topics beginning with $, such as $SYS/broker/..., are reserved for broker information. The specification says a filter starting with a wildcard (# or +) must not match them. To read them you must subscribe explicitly, for example $SYS/#. This prevents a debugging # subscription from being flooded with broker statistics. On a ConnectMQ broker, for example, connection state appears at $SYS/broker/connection/{clientId}/state. More on that in MQTT $SYS topics.
Try MQTT Wildcards on a Live Broker
The following experiment uses the public MQTT test broker at broker.connectmq.com. Replace w1c5 with your own random string so you see only your own messages, and keep payloads harmless because the public area is readable by everyone.
First, publish a small retained topic tree so that new subscribers receive it immediately:
B="-h broker.connectmq.com -p 1883 -i wc-pub-w1c5 -r -q 1"
R="connectmq/demo/w1c5"
mosquitto_pub $B -t "$R/home/kitchen/temperature" -m 21.5
mosquitto_pub $B -t "$R/home/kitchen/humidity" -m 48
mosquitto_pub $B -t "$R/home/garage/temperature" -m 14.2
mosquitto_pub $B -t "$R/home/garage/door" -m closed
mosquitto_pub $B -t "$R/home" -m house-online
mosquitto_pub $B -t "$R/office/desk/temperature" -m 23.0
Now run each subscription and compare the output (Ctrl+C after the retained messages appear):
S="-h broker.connectmq.com -p 1883 -i wc-sub-w1c5 -v"
R="connectmq/demo/w1c5"
# 1. Every temperature directly under a room
mosquitto_sub $S -t "$R/home/+/temperature"
# 2. Everything in the kitchen
mosquitto_sub $S -t "$R/home/kitchen/#"
# 3. Everything under home, including the home topic itself
mosquitto_sub $S -t "$R/home/#"
# 4. Only children of home, not home itself
mosquitto_sub $S -t "$R/home/+/#"
# 5. Temperatures in any building and room
mosquitto_sub $S -t "$R/+/+/temperature"
Expected results: filter 1 returns two topics; filter 2 returns two; filter 3 returns five, including home; filter 4 returns four; filter 5 returns three, including the office desk.
When you are done, clear the retained messages by publishing empty payloads with the retain flag:
for t in home/kitchen/temperature home/kitchen/humidity home/garage/temperature \
home/garage/door home office/desk/temperature; do
mosquitto_pub -h broker.connectmq.com -p 1883 -i wc-clean-w1c5 -r -n \
-t "connectmq/demo/w1c5/$t"
done
Wildcards in Python
import paho.mqtt.client as mqtt
ROOT = "connectmq/demo/w1c5"
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe([(f"{ROOT}/home/+/temperature", 0),
(f"{ROOT}/home/garage/#", 1)])
def on_message(client, userdata, msg):
# Topic matching helper: check which filter a topic belongs to
if mqtt.topic_matches_sub(f"{ROOT}/home/+/temperature", msg.topic):
print("Temperature", msg.topic, msg.payload.decode())
else:
print("Garage", msg.topic, msg.payload.decode())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="wc-py-w1c5")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()
A message that matches two of your filters may be delivered once or once per matching subscription, depending on the broker; the specification allows either behaviour. Design handlers to tolerate that.
Designing Topics So Wildcards Work for You
Wildcards are only as useful as the topic tree beneath them. Because + and # operate on whole levels, every decision about level order determines which questions a subscriber can ask cheaply. A few patterns pay off repeatedly.
Put the dimension you filter by most often near the front. If operators usually look at one site at a time, the site belongs early in the topic, so acme/berlin/# captures it in one filter. If they usually compare one measurement across all sites, keep the measurement as the final level so acme/+/+/+/temperature works.
Keep the depth consistent. A filter such as acme/+/+/status assumes every device sits exactly two levels below the namespace. If some devices are nested one level deeper, they silently drop out of every dashboard built on that filter. Consistent depth makes + predictable.
Separate message categories into their own level. With .../telemetry/..., .../state/... and .../cmd/... branches, a device can subscribe to acme/berlin/hall2/pump-3/cmd/# and receive only commands, and an analytics service can take only telemetry without ever seeing control traffic.
Do not encode multiple facts in one level. A level like berlin-hall2-pump3 cannot be split by wildcards; berlin/hall2/pump3 can.
Wildcards and Broker Load
Matching filters against topics is the broker's everyday work, and well-designed brokers handle wildcards efficiently. The cost you control is fan-out: a broad filter means more messages delivered to your client, more bandwidth and more processing. On metered or quota-limited plans, a narrow filter is also the cheaper filter. When a backend needs to process a broad branch, scale it with consumer groups instead of multiplying identical subscriptions.
Common Mistakes With MQTT Wildcards
- Subscribing to
#in production. It receives every message on the broker (or tenant), consumes bandwidth and may hit plan quotas. Use the narrowest filter that works. - Expecting
+to match several levels.home/+/temperaturewill not matchhome/kitchen/sensor1/temperature. Use a second+or restructure the topics. - Putting
#in the middle.home/#/temperatureis invalid and the subscription will be rejected. - Forgetting that filters are case-sensitive.
home/+/Temperaturewill not match lowercase topics. - Overlapping subscriptions. Overlaps can produce duplicate deliveries at different QoS levels.
- Hitting limits. The public broker allows 10 subscriptions per client and 8 topic levels, so prefer one well-placed wildcard over many exact subscriptions.
When a Wildcard Subscription Receives Nothing
Work through these checks in order: confirm the SUBACK reason code shows success rather than a rejected filter; print the exact topic the publisher uses and compare it level by level with your filter, including case and any leading slash; subscribe temporarily to the parent branch with # to see what is really being published; and finally check that the publisher and subscriber are connected to the same broker and tenant. Most "wildcards are broken" reports turn out to be one extra level or a capital letter.
Wildcards also combine with MQTT shared subscriptions: a filter such as $share/workers/factory/+/telemetry/# spreads a whole branch across a group of consumers. If you want the protocol basics first, read the MQTT tutorial.
Explore Topics with ConnectMQ
ConnectMQ is a cloud MQTT broker with a live topic explorer and an in-browser MQTT client in its portal, so you can test wildcard filters against your real topic tree without installing tools. Create a free Serverless broker, no credit card required, then connect your devices with YOUR_USERNAME, YOUR_PASSWORD and YOUR_CLIENT_ID over TCP, TLS or WebSockets.
Frequently Asked Questions
What is the difference between + and # in MQTT?
+ matches exactly one topic level and can appear anywhere in a filter. # matches zero or more levels and must be the last character of the filter.
Can I publish to a topic with a wildcard?
No. Wildcards are only allowed in subscription filters. Publishing to a topic containing + or # is a protocol violation.
Does # match $SYS topics?
No. Filters that start with a wildcard do not match topics beginning with $. Subscribe to $SYS/# explicitly to receive them.
Does home/# match the topic home?
Yes. The multi-level wildcard also matches the parent level, so home/# receives messages published to home as well as everything below it.



