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/* and event/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 retained irrigate command 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/online payload of false so 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