Topics are how MQTT decides which client receives which message. Because MQTT uses topic-based filtering (sometimes called subject-based filtering), the topic attached to every message is the entire basis for routing: the broker matches published topics against the topic filters that clients have subscribed to, and delivers accordingly.
One distinction is worth fixing in your mind from the start, because the rest of this article depends on it. A published message always uses a concrete topic name, a fully-qualified string with no wildcards. A subscription uses a topic filter, which is the same kind of string but may additionally contain wildcards to match a pattern of topics. The MQTT specification keeps these two terms separate, and so should you: you publish to topic names and you subscribe with topic filters. Getting topic structure right early is one of the highest-leverage decisions in an MQTT deployment, because subscriptions, wildcards, and access control all build on it.
This article explains what a topic is, how topic levels and the level separator work, how the single-level and multi-level wildcards match, the special role of topics beginning with $, and the practical conventions that keep a topic hierarchy clean and efficient. It is a technical reference; related operations such as subscribing and the publish/subscribe model itself have their own dedicated articles in this category.
Table of Contents
Topics at a glance
| Element | Symbol | Role |
|---|---|---|
| Level separator | / | Divides a topic into hierarchical levels |
| Single-level wildcard | + | Matches exactly one level (subscriptions only) |
| Multi-level wildcard | # | Matches all remaining levels (subscriptions only, must be last) |
| Reserved prefix | $ | Topics for broker-internal use, e.g. $SYS/ |
| Encoding | UTF-8 | Topics are UTF-8 strings, case-sensitive |
What a topic is
In MQTT, a topic is a UTF-8 encoded string, subject to MQTT’s UTF-8 validation rules, that the broker uses to filter messages for each connected client. Those rules forbid the null character (U+0000), malformed UTF-8, and certain invalid Unicode sequences, so not every byte sequence that is “text” is a valid topic. It is the address that a message is published to and the pattern that a client subscribes to. A topic consists of one or more topic levels, and each level is separated from the next by a forward slash, which is the topic level separator.
Take the topic myhome/groundfloor/livingroom/temperature. It has four levels: myhome, groundfloor, livingroom, and temperature, separated by three forward slashes. This hierarchical structure is what gives MQTT topics their power, because it lets subscribers select whole branches of the hierarchy at once using wildcards, rather than naming every individual topic.
A few foundational rules govern what a valid topic looks like:
- A topic must contain at least one character. An empty topic is not valid.
- Topics are case-sensitive.
myhome/temperatureandMyHome/Temperatureare two entirely different topics, and a subscription to one will not receive messages published to the other. - The topic string permits spaces, though as discussed in the best practices below, you should not use them.
- A lone forward slash (
/) is a valid topic. So is a topic with leading or trailing slashes, though again these are usually a mistake.
Here are several examples of valid topics, illustrating the range of what the hierarchy can express:
myhome/groundfloor/livingroom/temperature
USA/California/San Francisco/Silicon Valley
5ff4a2ce-e485-40f4-826c-b1a5d81be9b6/status
Germany/Bavaria/car/2382340923453/latitude
Notice that levels can be human-readable names, identifiers like UUIDs, or numeric IDs. The protocol does not impose meaning on the levels; the meaning is entirely a matter of the convention you and your subscribers agree on.
Topics are created on the fly
One of the most important practical properties of MQTT topics, and a key difference from a traditional message queue, is that topics are lightweight and require no setup. A client does not need to create a topic before publishing to it or subscribing to it. The broker accepts any syntactically valid topic that the client is authorized to use, without any prior initialization; where access-control lists are in force, a topic the client is not permitted to use will be rejected even though it is well-formed. The moment a client publishes to home/garage/door or subscribes to it, that topic effectively exists for routing purposes.
This is what makes MQTT topic hierarchies so flexible. You can introduce a new topic simply by publishing to it, and a hierarchy can grow organically as new devices and data types come online. The flip side of this flexibility is that there is nothing stopping a typo from creating an unintended topic, which is part of why disciplined naming conventions matter.
Wildcards
When a client subscribes to a topic, it can subscribe to the exact topic of a published message, or it can use wildcards to subscribe to many topics at once. This is the mechanism that lets a single subscription cover a whole family of topics.
The single most important rule about wildcards: wildcards can only be used when subscribing, never when publishing. A published message always goes to one specific, fully-qualified topic. Wildcards exist purely to let a subscriber express interest in a pattern of topics. Trying to publish to a topic containing a wildcard is invalid.
There are two kinds of wildcard, and they behave differently.
Single-level wildcard: +
The plus sign (+) is the single-level wildcard. As the name suggests, it replaces exactly one topic level, no more and no less. It can appear at any level of the topic, and a subscription can even contain more than one +.
Consider a subscription to myhome/groundfloor/+/temperature. The + occupies the third level, so it matches any single value there. Walking through some published topics:
myhome/groundfloor/livingroom/temperature— matches. The+matcheslivingroom.myhome/groundfloor/kitchen/temperature— matches. The+matcheskitchen.myhome/groundfloor/kitchen/brightness— no match. The final level isbrightness, nottemperature.myhome/firstfloor/kitchen/temperature— no match. The second level isfirstfloor, notgroundfloor.myhome/groundfloor/kitchen/fridge/temperature— no match. This topic has an extra level;+matches exactly one level, not several, so the structure no longer lines up.
The last two cases are the ones that most often trip people up. The + is rigid about position and count: it matches one level in its slot, and the rest of the topic must align exactly. It does not match across multiple levels, and it does not make other levels optional.
Multi-level wildcard:
The hash sign (#) is the multi-level wildcard. Where + covers exactly one level, # covers an arbitrary number of levels, including zero. There are two strict rules about where it can appear: the # must be the last character in the topic, and it must be preceded by a forward slash (except in the special case where the entire topic is just #).
When a client subscribes to a topic ending in a multi-level wildcard, it receives all messages on topics that begin with the pattern before the wildcard, no matter how long or deep those topics are. Consider a subscription to myhome/groundfloor/#:
myhome/groundfloor/livingroom/temperature— matches.myhome/groundfloor/kitchen/temperature— matches.myhome/groundfloor/kitchen/brightness— matches.myhome/firstfloor/kitchen/temperature— no match. The prefix before the#ismyhome/groundfloor, and this topic’s second level isfirstfloor.
The # matches all remaining levels beneath the prefix, at any depth. (This is a fixed structural rule, not regex-style greedy matching; it simply absorbs everything from its position onward.) This makes it ideal for subscribing to an entire branch of a hierarchy, for example all data from one floor of a building, or all topics belonging to one device.
Subscribing to everything:
If you specify only the multi-level wildcard as a topic, that is, you subscribe to # on its own, you receive every message that is sent to the broker. This is occasionally useful for debugging or for a logging client that genuinely needs to see all traffic.
However, subscribing to # alone is an anti-pattern in any system that expects significant throughput. A single client subscribed to # will receive a copy of every message on the broker, which can easily overwhelm it and impose a heavy routing and delivery burden on the broker. If you find yourself reaching for a bare # subscription in production, it is usually a sign that the design should be reconsidered: subscribe to the specific branches you actually need instead.
Combining wildcards
The two wildcards can be combined in a single subscription, with the constraint that #, if present, must still be last. A subscription such as myhome/+/temperature uses a single-level wildcard to match the temperature topic on every floor, while myhome/+/+/# would match a great deal more. In practice, the clearest and most maintainable subscriptions use the minimum wildcarding needed to capture exactly the topics of interest. Over-broad wildcards pull in more than intended and are harder to reason about.
Topics beginning with $
Generally you can name your topics however you like, but there is one important exception. Topics that begin with a dollar sign ($) have a special purpose and are treated differently by the broker.
These $-prefixed topics are not included when a client subscribes to the multi-level wildcard #. A subscription to # receives all ordinary application messages, but it does not receive messages on $-topics. This is deliberate: it prevents a broad # subscription from being flooded with broker-internal data.
The $-prefixed topics are reserved for the broker’s internal statistics and metadata. Many brokers reserve these topics for internal use and restrict client publishing to them, though the exact policy varies: some block client publishes entirely, some allow them with the right permissions, and some reserve only portions of the $ space. $SYS itself is not formally standardized by the MQTT specification; there is no official definition of which $-topics a broker must expose, so the specifics vary between implementations and you should not assume two brokers present identical $SYS trees. The widely-used convention is the $SYS/ prefix for broker statistics. Typical examples include:
$SYS/broker/clients/connected
$SYS/broker/clients/disconnected
$SYS/broker/clients/total
$SYS/broker/messages/sent
$SYS/broker/uptime
A subscriber can subscribe to $SYS/# to receive a broker’s internal statistics, which is a common way to monitor broker health. The key takeaways are that $-topics are for broker use rather than application data, that they are excluded from # subscriptions, and that their exact contents depend on the broker you are running.
How topic matching works, conceptually
It helps to hold a simple mental model of how the broker matches a published topic against subscriptions. When a message is published to a concrete topic, the broker walks that topic level by level against each subscription:
- A literal level in the subscription must match the corresponding level in the published topic exactly.
- A
+in the subscription matches whatever single level sits in that position. - A
#in the subscription matches the current level and every level after it, then matching stops, because#must be last.
A subscription matches the published topic only if the entire topic is accounted for: every level lines up under the rules above, with no leftover levels on either side (except where # absorbs the remainder). This level-by-level walk is why + is so strict about count and position, and why # must come last. It is also why topic structure matters for performance. Conceptually the broker matches every published message against its set of subscriptions; in practice, modern brokers optimize this heavily using subscription trees, tries, radix structures, and hash maps rather than checking subscriptions one by one. Even so, the complexity of the topic hierarchy still affects routing efficiency, so a sensibly structured, not-overly-deep hierarchy helps the broker do this work well at scale.
Best practices for topic design
MQTT topics are flexible, which is a strength, but the same flexibility means it is easy to create a hierarchy that is hard to read, hard to debug, and wasteful on the wire. The conventions below come from extensive real-world MQTT use and address the most common pitfalls.
Never use a leading forward slash
A leading forward slash is permitted. For example, /myhome/groundfloor/livingroom is a valid topic. However, the leading slash introduces an unnecessary topic level at the front, an empty level with a zero-length name, before myhome. That empty level provides no benefit, makes the topic one level deeper than it needs to be, and frequently leads to confusion when a subscription to myhome/# fails to match /myhome/... because the structures differ. Start your topics with a meaningful level, not a slash.
Never use spaces in a topic
A space is the natural enemy of every programmer. The topic string technically permits spaces, but they make topics much harder to read and to debug when something is not working as expected. Worse, UTF-8 has many different whitespace characters, some of which are visually indistinguishable from an ordinary space, so a stray non-breaking space in a topic can cause a subscription to silently fail to match with no obvious cause. As with leading slashes, the fact that something is allowed does not mean it should be used. Keep topics free of spaces and exotic whitespace.
Keep topics short and concise
Each topic is included in every message that uses it. The topic name travels on the wire with every single PUBLISH, so its length has a direct and repeated cost. On small, constrained devices and over low-bandwidth or metered links, every byte counts, and an unnecessarily long topic name multiplies that cost across every message sent. Make topic levels as short as they can be while remaining clear. This is also the motivation behind MQTT 5’s topic alias feature, which lets a client replace repeated topic strings with a short integer alias for the duration of a connection; the alias applies only within that connection, and sender and receiver maintain the alias-to-topic mapping as connection state. It is covered in its own article.
Use only ASCII characters, and avoid non-printable characters
Although topics are UTF-8 strings and can in principle contain any UTF-8 character, non-ASCII characters often display incorrectly in tools, logs, and dashboards, which makes typos and character-set issues extremely difficult to find. A topic that looks correct on screen might contain a character that is not what it appears to be. Unless it is absolutely necessary, restrict topic names to ASCII characters and avoid non-printable characters entirely. This keeps topics legible everywhere they appear and removes a whole class of hard-to-diagnose bugs.
Plan the hierarchy for growth
Because both publishers and subscribers must agree on the topic structure for subject-based filtering to work, the topic tree is something you design up front rather than discover at runtime. A well-planned hierarchy puts the more general levels first and the more specific levels last, so that wildcards can select meaningful branches. For instance, structuring topics as region/site/device/metric lets a subscriber easily request all metrics for a site (region/site/#) or one metric across all devices at a site (region/site/+/temperature). Design with the wildcards you will want to use in mind, and leave room for topics and levels you have not thought of yet, because changing a topic hierarchy after many publishers and subscribers depend on it is disruptive.
A worked matching example
To see the rules interact, take a single published topic and check it against several different subscriptions at once. Suppose a sensor publishes to:
factory/line1/machine3/temperature
Now consider seven clients, each with a different subscription, and whether each receives this message:
| Subscription | Receives it? | Why |
|---|---|---|
factory/line1/machine3/temperature | Yes | Exact match, level for level |
factory/line1/machine3/+ | Yes | + matches the single final level temperature |
factory/line1/+/temperature | Yes | + matches machine3 |
factory/+/+/temperature | Yes | Both + slots match line1 and machine3 |
factory/line1/# | Yes | # captures everything beneath factory/line1 |
factory/# | Yes | # captures everything beneath factory |
factory/line1/machine3 | No | The published topic has an extra level (temperature) that this exact topic does not account for |
factory/+/temperature | No | + matches one level, but the topic has two levels (machine3, temperature) where this expects one |
# | Yes | Matches every application topic on the broker |
The same single message can satisfy many subscriptions simultaneously, and each subscribing client receives its own copy. This is the everyday reality of an MQTT system: a publisher emits to one concrete topic, and the broker fans that message out to every subscription whose pattern matches, whether that pattern is exact, single-level wildcarded, multi-level wildcarded, or some combination. The two “No” rows are the instructive ones, and both come back to the same principle: a subscription matches only when the entire published topic is accounted for, with + consuming exactly one level and literal levels matching exactly.
Edge cases worth knowing
A few corner cases follow from the rules above and occasionally cause confusion.
Empty levels are real levels. Because the slash is a separator, a topic like home//temperature contains an empty middle level, and home/temperature/ has an empty trailing level. These are valid but almost never intentional, and they will not match the structurally different home/temperature. A leading slash, as noted in the best practices, creates an empty first level for the same reason. If a subscription mysteriously fails to match, an accidental empty level is a common culprit.
The single-level wildcard can match an empty level. Since an empty string is a valid level, home/+/temperature matches home//temperature, with the + matching the empty level between the two slashes. This is rarely something you rely on deliberately, but it is consistent with the rule that + matches exactly one level, whatever that level contains, including nothing.
Wildcards apply to retained-message delivery too. When a client subscribes with a wildcard, the broker sends it any retained messages on topics that match the wildcard, not just on exact topics. So a new subscription to home/# immediately receives the retained message from home/livingroom/temperature, if one exists, because the topic matches the pattern. This interaction between wildcards and retained messages is what lets a freshly connected client pull the last known state of a whole branch at once, and it is covered further in the retained messages article.
Common topic-design patterns and anti-patterns
Beyond the individual rules, a few recurring patterns separate hierarchies that age well from ones that become painful. These follow directly from how matching and wildcards work.
Order levels from general to specific. Put the broadest classification first and the most specific last, so that wildcards select coherent branches. A hierarchy like region/site/area/device/metric lets you address progressively narrower scopes: everything in a region (region/#), everything at a site (region/site/#), one metric across all devices in an area (region/site/area/+/temperature). If the levels were ordered specific-to-general, no useful wildcard subscriptions would be possible.
Keep a level’s meaning consistent across the hierarchy. Each position in the topic should mean the same kind of thing everywhere it appears. If the third level is “device id” for one branch, it should not be “metric name” in another. Inconsistent level semantics make wildcards unreliable, because a subscription like region/site/+/temperature assumes the + position always holds the same category of value.
Encode identity in levels, not in the payload, when you want to filter on it. If subscribers need to select messages by device or by metric, that identity belongs in the topic, where the broker can filter on it, not buried inside the payload, which the broker never inspects. Conversely, do not over-encode: data that no subscriber will ever filter on does not need its own topic level and can live in the payload.
Avoid the bare # subscription in production. As covered above, subscribing to # pulls every message on the broker to one client. It is fine for a deliberate debugging session or a dedicated logging sink, but as a routine pattern it creates a bottleneck and defeats the point of subject-based filtering. Subscribe to the specific branches you need.
Do not put volatile or unbounded values in early levels carelessly. Because the hierarchy is fixed by convention once publishers and subscribers depend on it, putting something that changes structure or explodes in cardinality near the top can make the tree hard to wildcard sensibly later. Think about which levels are stable classifications and which are high-cardinality identifiers, and place them accordingly.
Settle the structure early. Because both producers and consumers must agree on topic structure, and because changing the hierarchy after the fact means coordinating changes across every publisher and every subscriber that depends on it, the topic tree is worth designing carefully before a deployment grows. A little planning up front avoids a disruptive migration later.
How topics connect to the rest of MQTT
Topics are the foundation that several other MQTT features build on:
- Subscribing uses topic filters, with or without wildcards, as covered in the publish/subscribe operations article.
- The publish/subscribe model relies on subject-based filtering of topics, as covered in the pub/sub pattern article.
- Retained messages are stored per topic and matched to new subscribers using these same wildcard rules.
- Quality of Service is negotiated per subscription, which is per topic filter.
- Topic aliases (MQTT 5) exist precisely to mitigate the on-the-wire cost of long topic names.
- Shared subscriptions (MQTT 5) use a special
$share/<group>/<topic>form. Note that$shareis a subscription syntax, not a normal publish topic hierarchy: clients subscribe using this form, but messages are still published to ordinary concrete topic names, and the<topic>portion after the group is a regular topic filter that may itself contain wildcards.
Because routing, retention, QoS, and access control all key off the topic, a clean and well-structured topic hierarchy pays dividends throughout the entire system.
Frequently asked questions
What is a topic in MQTT?
A topic is a UTF-8 string that the broker uses to route messages. It is made of one or more levels separated by forward slashes. Publishers send messages to a specific topic, and subscribers express interest in topics, with or without wildcards; the broker delivers each message to the clients whose subscriptions match its topic.
What is the difference between the + and # wildcards?
The single-level wildcard + matches exactly one topic level in its position. The multi-level wildcard # matches all remaining levels from its position onward and must be the last character in the topic. Use + to wildcard one level and # to capture an entire branch.
Can I use wildcards when publishing?
No. Wildcards may only be used in subscriptions. Every published message goes to one specific, fully-qualified topic with no wildcards.
What are $SYS topics?
Topics beginning with $, commonly under the $SYS/ prefix, are reserved for the broker’s internal statistics and metadata, such as the number of connected clients or broker uptime. They are populated by the broker, are excluded from # subscriptions, and their exact contents vary by broker implementation.
Are MQTT topics case-sensitive?
Yes. Home/Temp and home/temp are different topics. A subscription to one will not receive messages published to the other.
Do I need to create a topic before using it?
No. MQTT topics require no setup. The broker accepts any valid topic on the fly, so a topic effectively exists as soon as a client publishes or subscribes to it.
Why should topics be kept short?
Because the full topic name is sent on the wire with every message that uses it. Long topics add overhead to every publish, which matters on constrained devices and low-bandwidth links. MQTT 5’s topic alias feature exists to reduce this cost for long, frequently-used topics.
