← Articles
MQTT/7 min read/ views

Designing Sparkplug B Topics You Won't Have to Rename Later

Laying out Sparkplug B group, edge node and device IDs — plus birth certificates, aliases and STATE — so a rename doesn't break every subscription.

MQTTSparkplugSCADATagsHistorianProject Notes

The expensive mistake with Sparkplug B isn't a bad payload — it's a group_id you picked in week one and have to live with for years. Rename it after three dashboards, a historian, and an analytics job are subscribed to spBv1.0/BrewhouseA/#, and you're chasing every one of those subscriptions to change a string. Nothing about the protocol stops you renaming it; everything about the deployment punishes you for it. So the topic namespace is the part worth slowing down on.

The five fields, and which ones are load-bearing

Sparkplug B fixes the topic structure so you don't invent your own:

spBv1.0/[group_id]/[message_type]/[edge_node_id]/[device_id]

message_type is not yours to design — it's one of NBIRTH, NDATA, NDEATH, DBIRTH, DDATA, DDEATH, NCMD, DCMD. The three you actually own are group_id, edge_node_id, and device_id, and they map cleanly to how you'll support the thing:

FieldMaps toThe trap
group_idSite / area / project boundaryEverything subscribes with group_id in the wildcard. Rename = re-subscribe everywhere. Pick it once.
edge_node_idThe gateway or IPC you can restart on its ownName it after the maintainable asset, not DESKTOP-7F3K or the integrator's laptop.
device_idA PLC, skid, meter, or drive behind that nodeOnly present on DBIRTH/DDATA/DDEATH. Use it when one node speaks for several logical devices.

If someone has to open a spreadsheet to work out which physical panel published a value, the namespace failed. Line3/PalletizerPLC beats L3N02/D14.

Draw edge-node boundaries where restarts hurt least

Don't collapse the whole plant onto one edge node just because a single gateway can reach every PLC. The boundary that matters is the restart blast radius: bouncing an edge node drops an NDEATH, and every device under it should go stale at the SCADA host. So the node should line up with something you'd actually restart as a unit — one line, one skid, one cell, one protocol gateway.

Too broad and a routine gateway reboot marks half the plant offline. Too fine and one small skid spawns forty edge nodes, forty birth certificates, and forty things to monitor. I aim for "the set of devices that share a failure and a maintenance window."

bdSeq and NDEATH are the whole point — set them up right

This is the part people skip and then wonder why the host shows ghost-online nodes after a network blip.

At MQTT CONNECT, the edge node registers its NDEATH as the Last Will and Testament. That message carries a bdSeq (birth/death sequence) metric. The matching NBIRTH carries the same bdSeq. That pairing is how the host tells "this NDEATH belongs to the session I currently think is alive" from "this is a stale will from an old session." Increment bdSeq on every new MQTT session (it's an 8-bit rolling value). Get this wrong and a reconnect leaves the host confidently displaying a dead node as online.

Two more numbers that trip people up:

  • The payload seq (0–255, wraps back to 0) must be 0 on NBIRTH and increment on every message after it. A gap tells the host it missed something and should re-request a birth. Don't reset it mid-session.
  • Metric aliases are assigned in NBIRTH (metric name → integer). NDATA then sends the alias instead of the full name to shrink the payload. So if a subscriber only ever sees NDATA, it can't decode anything — it has to have caught the birth. That's by design, and it's why birth reliability isn't optional.

Metric names are the tags people actually live in

Inside the payload, metrics are what your SCADA points, historian tags, and analysts touch every day. Name them for the person supporting the plant at 2 a.m., not for cleverness:

Pump101/RunFeedback
Pump101/StartCommand
Pump101/DischargePressure
Tank204/Level
Tank204/Level/HighAlarmActive
Mixer03/SpeedSetpoint

Keep them aligned with the HMI and PLC tag names. If the PLC symbol is P101_RunFb, the metric can read cleaner as Pump101/RunFeedback — but write that mapping down, because in eighteen months nobody will remember it.

And publish more than the raw value. 0 is not the same as bad quality, stale data, or a disconnected PLC. For anything that matters, include command and feedback state, auto/manual or local/remote mode, an interlock/permissive summary, alarm-active where it helps, and the comms status of downstream devices. Sparkplug metrics carry a quality property — use it rather than making the subscriber guess.

Don't make everything a one-second tag

MQTT makes publishing trivial, which is exactly the problem — a firehose of every register at 1 s ends up in the historian whether anyone reads it or not. Match the rate to the signal:

SignalWhat I publish
Equipment stateOn change, plus a slow heartbeat (e.g. 60 s) so the host knows the node is alive
Analog PVOn change with a deadband (2% is my usual starting point), plus a periodic refresh
Fast machine sequenceSummarized results or event states, not raw high-speed frames unless someone genuinely needs them
Runtime / energy countersPeriodic, plus on reset or rollover
Alarm stateOn change — and check ordering during a burst, because that's when it breaks

Decide the timestamp source explicitly: PLC clock, edge-node clock, or broker-side ingest time. They drift, and a historian that silently mixes them produces trends nobody trusts.

One thing worth remembering about retain and STATE

Sparkplug messages are published with retain = false — deliberately. A retained NDATA would hand a late subscriber a stale value with no birth context. The one exception is STATE: the SCADA host's own online/offline message (in Sparkplug 3.0, spBv1.0/STATE/[host_id] with a JSON online/timestamp payload) is retained, so edge nodes learn the host's status the moment they connect. If you see someone setting retain on data topics "so the dashboard fills in faster," that's a bug, not a feature.

Commissioning: test the failures, not the happy path

The happy path — broker up, gateway up, started in the right order — always works. Plants don't fail in the right order. So the checks that earn their keep:

  • Restart the edge node; confirm an NBIRTH with seq = 0 and a fresh bdSeq.
  • Kill the network to the node; confirm the host receives the NDEATH (the LWT) and marks devices stale — not just "last value forever."
  • Restart a single device; confirm the expected DBIRTH and that other devices on the node are untouched.
  • Confirm every expected metric appears in the birth payload, with stable data types across restarts (a metric that's Int32 in birth and Float in data will break a strict subscriber).
  • Fail the broker over and confirm both node and host recover without ghost-online state.

If the namespace, the birth/death handshake, and the retain rules are right, a good Sparkplug deployment is quiet in normal operation and loud in the exact way you want during a failure. Support staff should be able to answer three questions from the topic and the last birth alone: which asset sent this, when did it last prove its metric list, and is the value live, stale, or bad.