MQTT Wildcards and Dollar Topics: Audit Subscription Coverage at the Boundary
MQTT wildcards match topic levels, including empty levels, and a leading wildcard does not cover Topic Names beginning with a dollar sign. Build a concrete filter-versus-topic truth table before declaring that a subscription covers every device or every broker diagnostic. The most revealing cases are often a parent topic, a trailing slash or a system topic omitted by a broad hash filter.
This guide pins the OASIS MQTT 5.0 Standard, 7 March 2019, section 4.7. It also checks the actual matching and filter-validation APIs from Eclipse Mosquitto 2.0.20. A local C fixture linked to that release passed eighteen matching vectors and four malformed-filter cases. No broker subscription or authorization policy was exercised.
Preserve empty levels
A slash separates topic levels. Adjacent slashes create a zero-length level; they do not collapse into one separator. Leading and trailing slashes also affect the identity and matching structure.
Consequently, plant/a, plant//a and plant/a/ are different names. Do not run filesystem-style path cleanup, trim separators or normalize case before matching.
A plus wildcard occupies exactly one complete level. That level may be empty. It follows that plant/+ matches plant/, but not plant: the latter does not have the additional level.

Give hash its parent-level meaning
The multilevel wildcard matches the parent and any number of child levels. plant/# therefore matches plant, plant/ and deeper names such as plant/a/temperature.
Hash must be alone or follow a separator, and it must be the final character of the filter. A filter such as plant/#/temperature is invalid; it is not a way to search for a field at arbitrary depth.
If the application needs several depth-specific topic layouts, enumerate supported filters or redesign the naming contract. Do not quietly accept a nonstandard matcher that makes an invalid filter appear useful locally but behave differently at the broker.
Use a boundary truth table
| Filter | Topic Name | Match? | Reason |
|---|---|---|---|
| plant/+ | plant | No | No second level |
| plant/+ | plant/ | Yes | One empty level |
| plant/+ | plant/a/b | No | Too many levels |
| plant/# | plant | Yes | Hash includes parent |
| plant/+/temperature | plant//temperature | Yes | Empty middle level |
| # | $SYS/broker/uptime | No | Leading-dollar exclusion |
| $SYS/# | $SYS/broker/uptime | Yes | Explicit dollar prefix |
| plant/+ | plant/$internal | Yes | Dollar is not first character of the Topic Name |
The nested-dollar row is useful when a custom matcher overgeneralizes the system-topic rule. The exclusion concerns Topic Names beginning with dollar, not every level that happens to begin with it.
Audit system-topic coverage explicitly
Part 4.7.2 prohibits matching a filter beginning with hash or plus against a Topic Name beginning with dollar. A broad # subscription therefore does not imply visibility into $SYS/.
Use an explicit supported system-topic filter when those diagnostics are required, and confirm the broker actually publishes the expected nodes. The widespread $SYS convention is not a universal inventory of identical fields across all brokers.
A matching filter does not grant authorization. A broker can deny a subscription or a publication under its policy. Diagnose accepted-filter matching separately from access control; this guide does not specify an ACL design.
Validate filters independently from matching
The C fixture first exercised valid matching pairs through mosquitto_topic_matches_sub. It separately passed plant+, plant/#/x, plant/a# and plant/+suffix to the filter validator and required rejection.
Keep complete UTF-8 and field-length validation outside this narrow matching test. The string APIs and a successful wildcard check alone do not prove that every caller supplied a complete, valid MQTT field.
The fixture used Mosquitto 2.0.20 built locally with TLS and WebSockets disabled and linked its static client library using GCC 14.2. Those build choices do not change the cited matching rules, but they limit the execution evidence to the local APIs rather than transport interoperability.
Translate coverage requirements into test inputs
For a hypothetical gateway inventory, list each intended topic family and whether it includes a root summary, empty device level, optional suffix or broker diagnostic. Then write positive and negative examples before choosing filters.
If the requirement is “one temperature topic for each nonempty device identifier,” plus alone is too permissive because it also matches an empty level. Keep semantic device-identifier validation in the application or naming contract; wildcard matching does not express every business constraint.
Likewise, adding hash to make one missing message appear can broaden coverage far beyond the desired family. Inspect the negative rows, including command topics and future suffixes, before accepting that change.
Keep ordinary filter matching separate from shared-subscription wrapper parsing. This fixture did not test shared subscription groups or delivery distribution. If the deployed client adds a group prefix, validate that supported syntax first and apply the coverage matrix to the underlying topic filter rather than treating the wrapper as a literal topic path.
Release qualification
- Record the exact successful subscription filters, preserving bytes and options.
- Run parent, empty-level, deeper-level, case and trailing-separator fixtures.
- Test leading-dollar exclusion and explicit system-topic inclusion independently.
- Reject malformed wildcard placement before sending a subscription.
- Publish harmless uniquely identified lab messages to every positive and negative topic in the matrix.
- Compare actual broker delivery with the matcher result while retaining SUBACK and authorization evidence.
The local test established the eighteen stated matching outcomes, not a complete broker delivery guarantee. Acceptance requires the deployed filter set to cover the intended topic families and exclude the tested unintended ones, with system-topic visibility and empty-level behavior documented rather than assumed.