Sparkplug B for Industrial OT
TL;DR — Sparkplug B is an Eclipse Foundation spec on top of MQTT that adds: standard topic structure, Protobuf-encoded payloads, birth/death certificates, and stateful sessions. Use it for industrial OT where vendor interoperability matters. Skip for greenfield projects that don’t need to interop with industrial equipment.
After TimescaleDB ingest, one of the OT-specific topics: Sparkplug B. The name throws people. The spec is well-defined and increasingly common in factory environments.
What it is
Eclipse Sparkplug B is a layer on top of MQTT that defines:
- Topic namespace convention:
spBv1.0/<group_id>/<message_type>/<edge_node_id>/<device_id> - Payload format: Protobuf with a defined schema
- State management: birth/death certificates announce device lifecycle
- Stateful sessions: subscribers always know the current state of all devices
The standard ships out of Cirrus Link / Inductive Automation but is open. Multiple vendors implement it.
Topic structure
spBv1.0/<group_id>/<msg_type>/<edge_node_id>/<device_id>
Examples:
spBv1.0/factory-jakarta/NBIRTH/edge-gw-01
spBv1.0/factory-jakarta/DBIRTH/edge-gw-01/press-42
spBv1.0/factory-jakarta/DDATA/edge-gw-01/press-42
spBv1.0/factory-jakarta/DDEATH/edge-gw-01/press-42
spBv1.0/factory-jakarta/NDEATH/edge-gw-01
Message types:
- NBIRTH / NDEATH — edge node (gateway) coming online / going offline
- DBIRTH / DDEATH — device under that gateway online / offline
- NDATA / DDATA — data updates
- NCMD / DCMD — commands sent TO the device/node
Stateful: NBIRTH is sent retained when an edge gateway connects. Subscribers know the full structure. NDEATH is the will message — fires if the gateway disconnects ungracefully.
Payload — Protobuf
A simplified DDATA payload as Protobuf:
message Payload {
uint64 timestamp = 1;
repeated Metric metrics = 2;
uint64 seq = 3;
bytes uuid = 4;
bytes body = 5;
}
message Metric {
string name = 1;
uint64 alias = 2;
uint64 timestamp = 3;
uint32 datatype = 4;
bool is_historical = 5;
bool is_transient = 6;
bool is_null = 7;
oneof value {
uint32 int_value = 10;
uint64 long_value = 11;
float float_value = 12;
double double_value = 13;
bool boolean_value = 14;
string string_value = 15;
bytes bytes_value = 17;
}
}
A Sparkplug message carries multiple metrics. The seq field is a sequence number — subscribers detect gaps (missed messages).
Birth certificates
When an edge node connects:
- Sets MQTT will to
NDEATHfor<group>/<edge>topic - Publishes
NBIRTHwith full list of devices and metrics
Subscribers receiving NBIRTH know exactly what data to expect from this gateway. They build their state model. Subsequent DDATA messages are just updates to known metrics.
If the gateway dies, the will fires NDEATH. Subscribers know all devices under that gateway are stale.
This stateful model is the killer feature. With pure MQTT, a subscriber connecting late doesn’t know what topics exist. With Sparkplug, it can subscribe to spBv1.0/+/NBIRTH/# and discover everything.
Aliases for bandwidth
Repeating long metric names like factory/line1/press42/temperature in every message wastes bytes. Sparkplug supports aliases:
- NBIRTH defines
temperature: alias=1, name="Temperature" - Subsequent DDATA refers to metric by alias=1 only
- Saves significant bandwidth for high-frequency metrics
When to use Sparkplug B
Good fit:
- Factory environments using PLCs and industrial gateways
- Multi-vendor stacks where interop matters (Ignition SCADA, AVEVA, FactoryTalk)
- Projects that need device discovery without out-of-band metadata
- High-frequency telemetry where stateful sessions help with gap detection
Not a good fit:
- Pure greenfield where you control both sides
- Consumer IoT (smart home, wearables)
- Simple telemetry where vanilla MQTT JSON suffices
- Projects with no Protobuf tooling
Libraries
- Java: Eclipse Tahu (reference implementation)
- Go: github.com/EclipseSparkplug/sparkplug-client-go (somewhat early)
- Python: github.com/EclipseSparkplug/sparkplug-client-python
- C/C++: Eclipse Tahu C
Quality varies. Java is most mature. For new code in 2022, Java or Python is least painful.
Implementing on the edge
A simplified flow in Go (concept; library quality varies):
func main() {
client := newMQTTClient("ssl://broker:8883", "user", "pass", "spBv1.0/factory-jakarta/NDEATH/edge-gw-01")
// On connect: publish NBIRTH
client.OnConnect = func(c mqtt.Client) {
nbirth := buildNBIRTH(map[string]Metric{
"press-42/temperature": {DataType: Double, Value: 22.0},
"press-42/pressure": {DataType: Double, Value: 1.0},
})
c.Publish("spBv1.0/factory-jakarta/NBIRTH/edge-gw-01",
1, false, encodeProtobuf(nbirth))
}
// Periodic DDATA
go func() {
seq := uint64(0)
for range time.Tick(time.Second) {
seq++
ddata := buildDDATA(seq, map[string]Value{
"press-42/temperature": Float64(readTemperature()),
})
client.Publish("spBv1.0/factory-jakarta/DDATA/edge-gw-01/press-42",
0, false, encodeProtobuf(ddata))
}
}()
select {}
}
The Sparkplug spec adds significant complexity over vanilla MQTT JSON. The interop payoff is real if you need it.
Common Pitfalls
Using Sparkplug without need. Overhead doesn’t pay off for greenfield projects you control end-to-end.
Missing the will / NDEATH. The whole stateful contract relies on death certificates. Forget the will config and subscribers see ghost devices forever.
Sequence numbers not monotonic. Subscribers detect gaps and panic. Ensure your code increments correctly across restarts (persist seq if needed).
No alias usage on high-volume metrics. Bandwidth wasted on repeating names. Define aliases in NBIRTH.
Subscribing without state model. Sparkplug expects subscribers to track NBIRTH for context. Naive subscribers see DDATA with no idea what metric names mean.
Wrapping Up
Sparkplug B = MQTT for industrial OT with stateful sessions. Use when interop matters; skip otherwise. Friday: OPC UA to MQTT bridges — connecting actual PLCs.