Fundamentals

MQTT Topics and Wildcards: Design Guide and Best Practices

MQTT topics and wildcards explained: topic levels, the + and # wildcards, $SYS topics, shared subscriptions and best practices for designing a topic hierarchy.

Updated · 7 min read

In MQTT, the topic is the only thing that connects publishers and subscribers. There are no queues or exchanges to configure — the broker simply matches each message's topic against every subscription filter. That makes topic design the backbone of your system: it determines what clients can subscribe to, how permissions are expressed and how easy it is to add new consumers later.

Topic basics

A topic name is a UTF-8 string made of levels separated by a forward slash:

text
building-a/floor-3/room-12/temperature
vehicles/truck-0042/gps
acme/devices/sensor-17/status
  • Topics are case-sensitive. Home/Kitchen and home/kitchen are different topics.
  • No registration needed. A topic exists the moment someone publishes to it.
  • Every character counts. A leading slash creates an empty first level, so /home/kitchen is not the same as home/kitchen. Spaces are legal but cause endless confusion.
  • Size. Topic names must be at least one character and at most 65,535 bytes, and must not contain the null character.

Wildcards: + and #

Subscribers use topic filters, which may contain two wildcards. Publishers always use concrete topic names — wildcards in a PUBLISH are a protocol violation.

Single-level wildcard: +

+ matches exactly one level. It must occupy a whole level (sensor+ is invalid) but can appear anywhere and more than once.

FilterMatchesDoes not match
home/+/temperaturehome/kitchen/temperaturehome/kitchen/oven/temperature
+/+/statusacme/sensor-17/statusacme/status
home/+home/kitchenhome, home/kitchen/light

Multi-level wildcard: #

# matches any number of levels — including zero — and must be the last character of the filter, on its own level. home/# matches home/kitchen, home/kitchen/light/state and also the parent home itself. Filters like home/#/light or home# are invalid.

A lone # subscribes to every topic on the broker (except $ topics). It is useful for debugging your own private broker, but on a public broker it floods you with other people's traffic.

$SYS and other $ topics

Topics beginning with $ are reserved for the broker. The most common is $SYS/, where many brokers (Mosquitto, EMQX and others) publish statistics such as connected clients, message rates and uptime. The exact topics are broker-specific — the spec only reserves the prefix.

Wildcards at the first level do not match $ topics: # and +/monitor will never deliver $SYS/... messages. Subscribe explicitly:

bash
mosquitto_sub -h test.mosquitto.org -t '$SYS/broker/clients/connected' -v

MQTT 5 adds another reserved prefix: shared subscriptions use $share/<group>/<filter>. Every subscriber in the same group shares the stream, with each message going to only one member — a simple way to load-balance consumers. See MQTT 5 vs 3.1.1 for other MQTT 5 additions such as topic aliases.

Designing a topic hierarchy

A good hierarchy goes from general to specific, so that wildcards at the end select meaningful groups. A common pattern is:

text
<org-or-app>/<site>/<device-type>/<device-id>/<measurement-or-channel>

acme/berlin/thermostat/th-0193/temperature
acme/berlin/thermostat/th-0193/setpoint/set
acme/berlin/thermostat/th-0193/status

With this layout:

  • acme/berlin/# gives a site dashboard everything in Berlin.
  • acme/+/thermostat/+/temperature gives an analytics service every thermostat reading.
  • acme/berlin/thermostat/th-0193/# gives a single device's full stream.

Best practices

  1. Don't start with a slash. It adds a pointless empty level.
  2. Use lowercase, ASCII and hyphens. Avoid spaces and mixed case; they produce subtle mismatches.
  3. Put the device or client ID in the topic. It makes per-client access control easy (many brokers can substitute the client ID or username into ACL patterns).
  4. Separate data, commands and state. For example .../temperature for telemetry, .../setpoint/set for commands and .../status for retained state or Last Will presence.
  5. Keep topics short but descriptive. The topic is sent with every message (unless you use MQTT 5 topic aliases), so very long names cost bandwidth on constrained links.
  6. Don't encode data in the topic. Values belong in the payload; a topic per value creates unbounded topic counts and lots of retained messages.
  7. Version your schema if it may change. A prefix like v1/ lets old and new devices coexist during a migration.
  8. Avoid broad subscriptions in production. Subscribing to # in a backend service makes every message on the broker its problem.

Common topic mistakes

  • Expecting a/+ to match a. It does not — + requires a level to be present. Use a/# if you also want the parent.
  • Trailing slashes. home/kitchen/ has an empty last level and is a different topic from home/kitchen.
  • Overlapping subscriptions. If a client subscribes to both home/# and home/+/temperature, the spec lets the broker deliver either a single copy or one copy per matching subscription, so some brokers will hand you duplicates. Keep subscriptions disjoint where you can.
  • Mixing ID formats. If some devices publish as Sensor_17 and others as sensor-17, every filter and ACL rule has to handle both. Normalise IDs at provisioning time.
  • Treating topics as a database. Brokers are optimised for routing, not querying. If you need history, have a consumer subscribe and write messages to a database.

Topics and access control

Brokers authorise publish and subscribe operations per topic, so your hierarchy doubles as your permission model. If each device writes only under acme/<site>/<type>/<device-id>/, you can grant it exactly that subtree and nothing else. If topics are flat or inconsistent, writing tight rules becomes impossible and teams end up granting # to everyone.

Try it yourself

Open the online MQTT client, subscribe to testmqtt/demo/+/temperature and publish to testmqtt/demo/kitchen/temperature and testmqtt/demo/kitchen/oven/temperature. Only the first message arrives — exactly what the + rule predicts.

Try it in the online MQTT client

Frequently asked questions

What is the difference between + and # in MQTT?

The + wildcard matches exactly one topic level and can appear anywhere in a filter. The # wildcard matches any number of levels, including zero, and must be the last character of the filter.

Can I publish to a topic that contains a wildcard?

No. Wildcards are only valid in subscription filters. A PUBLISH with + or # in the topic name is a protocol error and the broker will close the connection.

Does subscribing to # include $SYS topics?

No. Filters that start with a wildcard do not match topics beginning with $. To receive broker statistics you must subscribe explicitly, for example to $SYS/#.