Sparkplug B: Stale Data, Missing Tags, and a Client Stuck Out of Sync
intermediate · 25 min
A Sparkplug B consumer showing frozen values or blank tags almost never has a network problem. It has a session problem. Here is how to tell a missed BIRTH from an alias mismatch from a duplicate node ID — and how to fix each without stopping the line.
ℹ️ Sparkplug B is not a stream of values — it is a session. The full tag list is sent exactly once, at connection, in a BIRTH message that is never retained. Everything after that is a delta that only makes sense if you caught that BIRTH. So when data looks stale, stop testing the network. Ask instead: did this consumer ever see the BIRTH, and is it still counting from the same place the edge node is?
A Sparkplug B session end to end — the NBIRTH that ships the alias map, NDATA that ships only numbers, a SCADA client that connected too late to resolve them, the NCMD rebirth that repairs it (same bdSeq, seq back to 0), and the registered NDEATH the broker fires when the node dies.
Why plain MQTT goes stale
MQTT is a delivery pipe. The broker forwards a published message to whoever is subscribed at that instant. It stores no tag list, no data types, no history. Connect one second after a value was published and you get nothing — you wait for the next change. If a tag only moves once a shift, your screen sits blank or shows a default for hours. That is not a fault. That is MQTT working as designed.
Retained messages patch exactly one message per topic. They do not describe a machine.
What Sparkplug B adds
Define these before you troubleshoot:
- Edge node — the gateway or device that publishes on behalf of itself and its attached devices.
- Edge node descriptor —
group_id+edge_node_id. This pair is the node's address and must be unique across the whole Sparkplug system. - BIRTH certificate —
NBIRTH(node) andDBIRTH(device). The full tag set: every metric the node will ever report, each carrying at minimum a name, a data type and a current value. Aliases are optional; if the node uses them, the BIRTH carries name and alias together. - Alias — an optional integer that stands in for a long tag name, to shrink every later message.
- NDATA / DDATA — report-by-exception. Only metrics that changed since the last BIRTH or DATA. When aliases are in use, the name is omitted and only the alias is sent — and the same rule applies to
NCMD/DCMD. - NDEATH / DDEATH — death certificates. The
NDEATHis registered as the MQTT Will Message inside the CONNECT packet, so the broker publishes it when the connection drops ungracefully. Detection is not instant: the broker only fires the Will once the keep-alive lapses (roughly 1.5 × the keep-alive interval), so a 60 s keep-alive means ~90 s to notice. That interval is the knob, not the protocol. - seq — a counter 0–255 on every message from that edge node, wrapping back to zero after 255. The NBIRTH carries
seq = 0. - bdSeq — a birth/death sequence number that ties one NBIRTH to its matching NDEATH, so you know which session ended. It starts at zero, increments by one on every new MQTT CONNECT, and wraps to 0 after 255.
- STATE — every Sparkplug Host Application publishes
spBv1.0/STATE/<host_id>, retained, at QoS 1, carrying JSON withonline(boolean) andtimestampkeys (Sparkplug 3.0 format; 2.2 used a plainONLINE/OFFLINEUTF-8 string). "Primary" is a role assigned in the edge node's configuration — an edge node reacts only to the one host ID it is configured to watch, and ignores every other STATE topic on the broker. - Payload encoding — Google Protocol Buffers, a binary format. A plain text MQTT client shows you bytes, not values.
The QoS and retain rules, in full
Every edge-node message — NBIRTH, DBIRTH, NDATA, DDATA, NDEATH, DDEATH, NCMD, DCMD — is published QoS 0, retain false. There are exactly two exceptions in the protocol:
| Message | QoS | Retain |
| All edge-node messages | 0 | false |
| Edge node's Will (the NDEATH registered in CONNECT) | 1 | false |
| Host Application STATE (birth and Will) | 1 | true |
Read the first row twice. The message that defines your entire tag set is fire-and-forget and never stored. Miss it and it is gone.
Tell the causes apart first
| Symptom | Look for | Cause |
| Tags exist but values frozen; other consumers are fine | Your client connected after the node did | Missed BIRTH — you subscribed mid-stream |
| Values move but land on the wrong tags, or numbers are nonsense | A recent NBIRTH from that node | Alias mismatch — you are decoding against an old alias map |
| Constant seq gaps, values flipping between two plausible readings, repeated rebirth requests | Two publishers on the same topic | Duplicate edge node descriptor |
| Whole node greys out at once, roughly one keep-alive after the event | An NDEATH with a bdSeq matching the last NBIRTH | Connection loss — real, and correctly reported |
| Every node on one broker goes stale together | STATE for the configured primary host shows online: false | Primary host went away |
Step-by-step
- Subscribe with a decoder, not a text client. Wireshark ships a Sparkplug B dissector implemented as a heuristic subdissector of MQTT that hands the payload to the protobuf dissector; MQTT Explorer has a built-in Sparkplug B decoder. If your tool shows binary garbage, you are reading protobuf raw — change the tool, do not conclude the payload is corrupt. Note that neither helps on TLS: if the broker is on 8883 the capture is opaque without keying material, so capture on the plaintext listener or decode at a client instead.
- Read the STATE topics. Subscribe to
spBv1.0/STATE/#. Three outcomes:
- Nothing at all — normal in systems with no Host Application configured as primary. Nothing to conclude; go to step 3.
online: truefor the host your edges are configured to watch — not your problem; go to step 3.online: false— the edges are doing what the spec tells them to. An edge node configured for that primary host must, on receiving an offline STATE whosetimestampis greater than or equal to the prior online STATE's timestamp, immediately publish its NDEATH, disconnect, and move to the next MQTT server in its list. Everything downstream will look dead and the edges are correct. Check the timestamp before you accept this: an offline STATE with an older timestamp must be ignored by the edge nodes, and a stale retained one that nobody cleared is its own bug.
- Confirm whether a BIRTH exists at all. Subscribe to
spBv1.0/+/NBIRTH/+and watch. If you see DATA on a node but no BIRTH in your session history, you joined mid-stream. This is the single most common cause. - Check seq continuity. Log
seqper edge node descriptor — one counter covers all of that node's messages, node and device alike. Clean systems increment by one and wrap 255 → 0. Sporadic gaps under load point to reordering; gaps that never resolve point to two publishers. If your host software offers a Reorder Timeout parameter, that is the window it waits before giving up and requesting a rebirth; the spec only recommends providing the parameter and mandates no value, so a host that never rebirths may simply have none configured. - Match bdSeq across NBIRTH and NDEATH. If an NDEATH's
bdSeqdoes not match the NBIRTH you are holding, the death belongs to an older session and your view is a session behind. bdSeq changes only on a new MQTT CONNECT — a rebirth reuses the same value, so do not read an unchanged bdSeq after step 6 as a failed rebirth. - Request a rebirth — but read this first. On receiving a rebirth command the edge node must immediately stop sending DATA messages and republish the full BIRTH sequence. Values on every consumer pause for that window, and quality may flip to bad, which can trip alarms or any interlock reading those tags. Confirm with the operator before you do it. Then publish an
NCMDtospBv1.0/<group_id>/NCMD/<edge_node_id>with a metric namedNode Control/Rebirthset totrue. This is the one deliberate exception to the alias rule: every NBIRTH must carry a BooleanNode Control/Rebirthmetric with valuefalse, and when the node uses aliases that metric must be published without an alias — precisely so any host can request a rebirth by name without a valid alias map. Every other NCMD on an aliased node goes the other way: alias only, name excluded. - Fix an alias mismatch by discarding, not remapping. On every NBIRTH, throw away the entire alias table for that node and rebuild it from the new BIRTH. Aliases are only valid within the session that declared them.
- Resolve duplicate node IDs at the source. Compare
group_id/edge_node_idin both gateway configs. If they also share an MQTT client ID, the broker will disconnect the existing client whenever the other connects — you will see a connect/NDEATH loop. Change theedge_node_idon one gateway. Warning: restarting a gateway drops its publishing session and fires an NDEATH; anything consuming those tags goes bad-quality until the new BIRTH lands. - Verify by watching one known tag change on the machine and confirming the value arrives with a
seqthat follows the previous one.
Never do this
Never publish to an edge-node Sparkplug topic — spBv1.0/<group_id>/<VERB>/<edge_node_id> — with the retain flag set. A mosquitto_pub -r to one of those during testing is the expensive mistake. The spec requires every edge-node message to be non-retained and consumers assume it. (The one topic that is retained by design is spBv1.0/STATE/<host_id>, published retained at QoS 1 by the Host Application itself — leave it alone; do not hand-publish to it either.)
A retained message on a data topic silently replays a stale value to every client that ever subscribes again. A retained message on an NCMD or DCMD topic is worse: because Sparkplug clients connect with Clean Session true (or Clean Start true with Session Expiry 0 on MQTT 5.0) they re-subscribe on every reconnect, so the edge node re-receives that command every single time it reconnects — a test setpoint or a control write firing again days later, unattended, with nobody at the keyboard. Nothing clears it on its own unless the publisher happened to set an MQTT 5.0 message-expiry interval. You must publish a zero-byte retained message to that exact topic to remove it — and finding which topic was poisoned, months later, on a running plant, is a very bad afternoon.
Also: never hardcode an alias-to-tag map captured from one BIRTH into a consumer. And never restart the broker to "clear" stale data — that drops every session at once and forces every edge node to reconnect and rebirth simultaneously.
Key points
- Plain MQTT stores no tag list or history — a client that subscribes after a value was published simply waits, which is why late joiners look frozen rather than broken.
- The NBIRTH/DBIRTH certificate carries the complete tag set with names, data types, values and aliases, and in Sparkplug 3.0 it is published non-retained at QoS 0 — miss it and it is gone.
- NDATA/DDATA are deltas that carry only the alias, so they are meaningless to a consumer holding no BIRTH or a stale alias map.
- NDEATH is registered as the MQTT Will Message in the CONNECT packet, so the broker announces an ungraceful disconnect immediately instead of leaving consumers on a poll timeout.
- The seq counter runs 0-255 and wraps; an unresolvable gap means either message reordering or two publishers sharing one edge node descriptor.
- bdSeq ties an NDEATH back to the NBIRTH of the same session — a mismatch means you are looking at a death from an older session.
- A rebirth request is an NCMD carrying Node Control/Rebirth = true; the node must stop sending DATA and republish the full BIRTH, so values pause and quality can go bad during that window.
- The payload is Google Protocol Buffers, so use Wireshark's Sparkplug B dissector or MQTT Explorer's decoder — a plain text client shows binary, not corruption.
- group_id + edge_node_id must be unique system-wide; duplicates produce interleaved seq numbers and values that flip between two machines.
- The primary host publishes retained JSON with online and timestamp on the STATE topic, and edge nodes configured for that host must disconnect when it reports offline.
Codes and symptoms
- NBIRTH — Node birth certificate
- Published when an edge node establishes its Sparkplug session. Carries every metric the node will ever report, each with at minimum a name, data type and current value — plus an alias only if the node uses aliasing. Also carries the mandatory Boolean Node Control/Rebirth metric (value false) and the bdSeq for this session. Sent with seq = 0, non-retained and QoS 0, so a consumer that was not subscribed at that moment never receives it.
- DBIRTH — Device birth certificate
- Same role as NBIRTH but for a device attached behind the edge node. Must follow its NBIRTH, and is likewise QoS 0 and non-retained. A host that receives a DBIRTH before any NBIRTH from that node has a documented reason to issue a rebirth request.
- NDEATH — Node death certificate (the MQTT Will)
- Registered inside the CONNECT packet as the MQTT Will Message — the one edge-node message published at QoS 1 (retain false) — so the broker publishes it when the connection drops ungracefully, roughly 1.5x the keep-alive interval after the drop. An edge node also publishes its own NDEATH before disconnecting deliberately, such as on a valid offline STATE from its configured primary host. Contains the bdSeq of the session that ended — match it against your held NBIRTH to confirm which session died. If the bdSeq does not match, your view is a session behind.
- DDEATH — Device death certificate
- Signals that one device behind an edge node has gone away while the node itself is still connected. Distinguishes a single failed device from a whole gateway dropping off.
- seq — Sequence number gap
- Every message from an edge node carries seq, 0-255, wrapping back to zero after 255; one counter covers that node's own and its devices' messages. A gap means at least one message was lost or reordered. A host configured with a Reorder Timeout starts that window on the out-of-order message and, if the missing message never arrives, must issue a rebirth request — hosts are only recommended, not required, to provide that parameter, so a host that never rebirths may simply have none set. Gaps that never settle indicate two publishers sharing one edge node descriptor.
- bdSeq — Birth/death sequence mismatch
- A metric present in both the NBIRTH and the NDEATH Will payload. It starts at zero, increments by one on each new MQTT CONNECT and wraps to 0 after 255, and correlates a death certificate with the exact birth it terminates. Use it to reject stale deaths belonging to a previous session. A rebirth reuses the same bdSeq — it is not a new MQTT session — so an unchanged value after a rebirth request is correct, not a failure.
- Node Control/Rebirth — Rebirth request metric
- A Boolean metric that every NBIRTH must include, published false. A host writes it true via NCMD to force a full BIRTH resend. When the node uses aliases this metric must NOT carry an alias, and the rebirth NCMD identifies it by name — the deliberate exception to the rule that NCMD/DCMD send alias only and omit the name — so any host can request a rebirth without a valid alias map. On receipt the node must immediately stop sending DATA and republish the complete BIRTH sequence, reusing the same bdSeq.
- STATE online=false — Configured primary host offline
- Retained JSON at QoS 1 on spBv1.0/STATE/<host_id>, containing online and timestamp keys. Every Sparkplug Host Application publishes one; 'primary' is a role set in the edge node's configuration, so a node reacts only to the single host ID it is configured to watch. When that host's STATE is online=false with a timestamp greater than or equal to the previous online STATE's timestamp, the edge node must immediately publish its NDEATH, disconnect from the current MQTT server and try the next one in its list; an offline STATE with an older timestamp must be ignored. Every node on one broker going stale at once points here first — as does a stale retained STATE that nobody cleared.