MQTT starts simple and becomes chaotic quickly if naming, ownership, and evolution rules are not defined. Topic governance is not bureaucracy. It is how teams avoid accidental coupling and fragile integrations.
1. Topic hierarchy principles
A practical hierarchy typically encodes:
- environment
- site or region
- device class
- device ID
- channel purpose (state, command, event)
Make hierarchy semantic but not overly deep. The scheme I keep coming back to across projects is a fixed, positional prefix:
{env}/{site}/{device-class}/{device-id}/{channel}/{measurement}
A few concrete instances from that pattern:
prod/budapest-01/soil-node/sn-00a3/telemetry/moisture
prod/budapest-01/soil-node/sn-00a3/telemetry/battery
prod/budapest-01/soil-node/sn-00a3/state/online
prod/budapest-01/soil-node/sn-00a3/command/irrigate
prod/budapest-01/gateway/gw-01/event/fault
The rules I enforce for that scheme:
- Every segment is lower-case, ASCII, hyphenated. No spaces, no unit symbols, no localized text.
{env}is a small closed set (dev,staging,prod) so a wildcard on environment is meaningful.{device-id}is a stable identifier, not a MAC address or an index that changes on re-flash.- The
{channel}segment is always one of a fixed vocabulary (telemetry,state,command,event). Consumers and ACLs key off that segment, so it must be positionally reliable. - Depth stops at six or seven levels. Deeper trees look tidy but make wildcards and ACLs painful.
Put the fixed segments early. Wildcards and ACLs read left to right, so the more stable a value, the closer to the root it belongs.
2. Separate telemetry, command, and lifecycle
Do not mix data types in one topic family. Keep clear boundaries:
- telemetry: periodic measurements
- command: requested actions
- lifecycle: online/offline, version, health
This improves ACL control and consumer logic.
Retention policy follows the channel, and getting this wrong is a common source of confusion:
state/*andevent/fault-style topics are retained with QoS 1. A late subscriber needs to know the last known online/offline status or fault latch without waiting for the next publish.telemetry/*is not retained. A retained temperature reading is stale the moment it lands; a new subscriber getting a five-minute-old value as if it were live causes real bugs downstream.command/*is not retained. A retained command replays on every reconnect and re-triggers actions — I have seen a retainedirrigatecommand re-fire a pump on every broker reconnect. Use it with QoS 1 and an explicit request ID instead.- Last Will and Testament (LWT) publishes a retained
state/onlinepayload offalseso the broker announces a dropped device even when it disconnects ungracefully.
3. Payload contract ownership
Every topic family needs an owner team and schema definition. Include:
- required fields
- units and precision
- timestamp semantics
- compatibility rules
No owner means undocumented breaking changes.
4. Versioning strategy
When payload changes are unavoidable:
- prefer backward-compatible additions
- introduce explicit version channel when breaking
- deprecate old versions with timeline
Silent schema drift is one of the hardest integration failures to detect.
5. ACL model aligned with namespace
Topic namespace should support security policy directly:
- device can publish only own telemetry path
- device can subscribe only its command path
- admin tools have audited elevated scope
If namespace and ACL goals conflict, redesign namespace.
Because the namespace puts stable segments first and encodes the device ID explicitly, the ACL can use pattern substitution instead of one rule per device. In Mosquitto, %u expands to the authenticated username; I provision each device with a username equal to its {device-id}:
# Per-device rules (Mosquitto ACL file, %u = authenticated username)
# A device may publish only its own telemetry, state, and events...
pattern write prod/+/+/%u/telemetry/#
pattern write prod/+/+/%u/state/#
pattern write prod/+/+/%u/event/#
# ...and subscribe only to its own command channel.
pattern read prod/+/+/%u/command/#
# Backend/ingest service account: read all telemetry and state, no command write.
user ingest
topic read prod/#
# Operator tooling: scoped, audited, and separate from device credentials.
user operator
topic readwrite prod/+/+/+/command/#
The pattern keyword means one rule set covers the whole fleet; a device physically cannot publish under another device's ID because its username does not match %u. Command authority lives only in the operator/service accounts, never on the devices themselves.
6. Wildcard usage policy
Wildcards are useful but dangerous at scale. Restrict wildcard consumers to infrastructure tools and monitored services.
Application services should subscribe narrowly when possible.
7. Documentation and linting
Treat topic catalog as code:
- machine-readable registry
- schema lint checks in CI
- example payloads per topic
Automated checks prevent accidental naming regressions.
The naming rules I hold the line on, and why each one earns its place:
| Rule | Example | Reason |
|---|---|---|
| Fixed segment order | {env}/{site}/{class}/{id}/{channel}/{measurement} |
Wildcards and ACLs depend on position |
| Closed environment set | dev, staging, prod |
Makes prod/# wildcards and isolation reliable |
| Lower-case, hyphenated, ASCII | soil-node, not Soil Node |
Cross-platform, avoids encoding surprises |
| Stable device IDs | sn-00a3, not a re-flash counter |
Identity must survive firmware updates |
| Channel from fixed vocabulary | telemetry / state / command / event |
Consumers and ACLs branch on this segment |
| No units in topic names | measurement in payload, not .../temp-celsius |
Units are a payload/schema concern |
Enforce these in CI against the topic registry so a typo like telemtry fails the build instead of silently creating a second, unmonitored topic tree.
8. Migration playbook
For major restructures:
- run dual-publish period
- track consumer migration progress
- remove legacy topics only after explicit cutover approval
A forced overnight switch usually causes hidden data loss.
Final note
Good MQTT governance keeps systems evolvable. Naming consistency, ownership, and compatibility policy are foundational once projects move beyond a handful of devices.
Sources
- MQTT Version 5.0 specification (OASIS Standard): https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
- Eclipse Mosquitto ACL /
mosquitto.confdocumentation: https://mosquitto.org/man/mosquitto-conf-5.html - Sparkplug B specification (Eclipse Foundation), for teams standardizing topic and payload structure: https://sparkplug.eclipse.org/specification/
- Imported from the previous version of the site (gaborl.hu). Original publication around 2024-05-11.
Update history
- — Adopted from previous gaborl.hu (Hugo) site via legacy import.