Industrial IoT

Report by Exception (RBE) in MQTT Systems: Deadbands, Heartbeats and a Gateway Example

Report by exception publishes a value only when it changes meaningfully. Learn how deadbands, rate limits and heartbeats work together, and run a Python gateway that applies RBE before publishing to MQTT.

Chart comparing polled samples with report by exception publishes after a deadband filter in an MQTT gateway
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Report by exception (RBE) is the practice of publishing a value only when it changes by a meaningful amount, instead of sending every polled sample. It is one of the simplest and most effective techniques in industrial MQTT systems. In this article you will learn how deadbands, minimum intervals and heartbeats fit together, and run a Python gateway that applies RBE before publishing to a broker.

Polling vs Report by Exception

Traditional SCADA and many Modbus or OPC UA integrations work by polling: every tag is read on a fixed cycle and every result is forwarded. That is simple, but most industrial values change slowly. A tank temperature read every second for an hour produces 3,600 messages that may contain only a handful of meaningful changes.

MQTT is designed for the opposite style. Clients publish when something happens, and the broker delivers the new value to every subscriber. RBE combines the two: the gateway still polls the device locally (because the device protocol requires it), but it publishes upstream only when the value has changed enough to matter.

Approach Messages sent Freshness Typical risk
Fixed-interval publishing Every sample Predictable Wasted bandwidth, storage and processing
Pure report by exception Only on change Immediate for changes A silent value looks the same as a dead device
RBE with heartbeat Changes plus periodic refresh Immediate, with bounded staleness Requires a little more logic

The third row is what you want in practice.

The Building Blocks of Report by Exception

Deadband

A deadband is the minimum change that counts as "different". It filters out sensor noise and insignificant drift.

  • Absolute deadband: publish if the absolute difference between the new value and the last published value is at least, for example, 0.5 degrees.
  • Percent deadband: publish if the change exceeds a percentage of the last value or, more robustly, of the instrument span. Span-based percent is safer near zero, where a percentage of the value would make tiny fluctuations look huge.

Always compare against the last published value, not the last sampled value. Comparing to the previous sample lets a slow drift creep through unnoticed forever, because each step is below the deadband.

Minimum Interval (Rate Limit)

A noisy or oscillating signal can still cross the deadband repeatedly. A minimum interval, for example one second per tag, caps the rate. Set it carefully for alarms and state changes: you usually want those immediately, so give them a minimum interval of zero.

Heartbeat (Maximum Interval)

If a value never changes, RBE never publishes it, and consumers cannot tell "steady" from "gateway dead". A heartbeat publishes the current value after a maximum silence, for example every five minutes, even without a change. Combined with an MQTT Last Will on the gateway's status topic, subscribers can always distinguish a quiet value from a lost connection.

Quality Changes

A change in data quality (a sensor read failing, a device going offline) should always be published, regardless of the deadband. A stale "GOOD" value that is actually from a failed device is worse than no value.

Choosing Deadbands: Practical Guidelines

  1. Start from the instrument's accuracy. A deadband smaller than the sensor's accuracy just forwards noise.
  2. Ask the consumer what resolution it needs. A dashboard showing whole degrees does not need 0.01 degree changes.
  3. Use zero deadband for discrete values: states, modes, alarms, counters and setpoints.
  4. Be careful with totalisers and energy counters. Use a deadband in units of the counter and a regular heartbeat so billing-style calculations stay accurate.
  5. Review deadbands after go-live by comparing message rates per tag; a few chatty tags often dominate traffic.

A Python Gateway with Report by Exception

This example simulates five tags read every 500 ms, applies per-tag deadband, minimum interval and heartbeat rules, and publishes changes as retained JSON messages to the public test area of broker.connectmq.com. It prints how many samples were read versus how many messages were sent, so you can see the effect directly. It uses paho-mqtt 2.x and Python 3.10 or newer.

import json
import random
import time
from dataclasses import dataclass
from datetime import datetime, timezone

import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PORT = 1883
CLIENT_ID = "rbe-gateway-6e41"
BASE = "connectmq/demo/rbe-6e41/plant1/boiler1"
POLL_S = 0.5


@dataclass
class Tag:
    name: str
    deadband: float            # absolute, in engineering units (0 = any change)
    min_interval: float = 1.0  # seconds between publishes
    heartbeat: float = 300.0   # seconds, publish even without change
    last_value: float | None = None
    last_quality: str | None = None
    last_ts: float = 0.0

    def should_publish(self, value, quality, now):
        if self.last_value is None or quality != self.last_quality:
            return True                                   # first value or quality change
        if now - self.last_ts >= self.heartbeat:
            return True                                   # heartbeat
        if now - self.last_ts < self.min_interval:
            return False                                  # rate limit
        if self.deadband == 0:
            return value != self.last_value
        return abs(value - self.last_value) >= self.deadband

    def mark(self, value, quality, now):
        self.last_value, self.last_quality, self.last_ts = value, quality, now


TAGS = {
    "temperature_c": Tag("temperature_c", deadband=0.5),
    "pressure_bar":  Tag("pressure_bar", deadband=0.05),
    "flow_m3h":      Tag("flow_m3h", deadband=0.2),
    "burner_on":     Tag("burner_on", deadband=0, min_interval=0),
    "alarm_code":    Tag("alarm_code", deadband=0, min_interval=0),
}

state = {"temperature_c": 82.0, "pressure_bar": 3.20, "flow_m3h": 12.0,
         "burner_on": 1, "alarm_code": 0}


def poll_device():
    """Simulate a device read: small noise plus occasional real changes."""
    state["temperature_c"] += random.gauss(0, 0.08)
    state["pressure_bar"] += random.gauss(0, 0.008)
    state["flow_m3h"] += random.gauss(0, 0.03)
    if random.random() < 0.005:
        state["burner_on"] = 1 - state["burner_on"]
    quality = "BAD" if random.random() < 0.002 else "GOOD"
    return {k: round(v, 3) for k, v in state.items()}, quality


def on_connect(client, userdata, flags, reason_code, properties):
    print("Connected:", reason_code)
    client.publish(f"{BASE}/status", "online", qos=1, retain=True)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
client.connect(BROKER, PORT, keepalive=60)
client.loop_start()

samples = sent = 0
started = time.monotonic()
try:
    while True:
        values, quality = poll_device()
        now = time.monotonic()
        for name, value in values.items():
            samples += 1
            tag = TAGS[name]
            if tag.should_publish(value, quality, now):
                payload = {
                    "v": value,
                    "q": quality,
                    "ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
                }
                client.publish(f"{BASE}/{name}", json.dumps(payload), qos=1, retain=True)
                tag.mark(value, quality, now)
                sent += 1
        if now - started >= 60:
            print(f"last 60 s: {samples} samples read, {sent} messages published "
                  f"({100 * sent / samples:.1f} %)")
            samples = sent = 0
            started = now
        time.sleep(POLL_S)
except KeyboardInterrupt:
    client.loop_stop()
    client.disconnect()

Watch the result:

mosquitto_sub -h broker.connectmq.com -p 1883 -i rbe-6e41-viewer -t "connectmq/demo/rbe-6e41/#" -v

The once-a-minute summary line shows how much of the polled data actually needed to be sent. With realistic deadbands, the reduction is large, and the remaining messages are the ones that carry information. Tune the deadbands and watch the ratio change.

Why Retained Messages Matter with Report by Exception

When values are published only on change, a subscriber that connects now might otherwise wait minutes for the next update. Publishing each tag as a retained message solves that: the broker stores the last value per topic and delivers it immediately to every new subscriber. One topic per tag works better than one big JSON blob for this, because each tag keeps its own retained last value. If you need a refresher on delivery guarantees, see MQTT QoS levels explained.

RBE in Sparkplug B

Report by exception is built into the Sparkplug B specification. Edge nodes send a full birth certificate with every metric when they connect, then send only changed metrics in DATA messages. If a host application loses track, it requests a rebirth. That gives Sparkplug the same "full state on connect, changes afterwards" behaviour as retained messages plus RBE in plain MQTT. ConnectMQ can decode Sparkplug B payloads and republish them into readable ISA-95 topics, so RBE-optimised Sparkplug traffic remains easy to inspect.

Where RBE Pays Off Most

  • Metered or constrained links. On cellular and satellite connections, every byte counts. RBE is usually the first optimisation to apply, as described in MQTT over cellular.
  • Store-and-forward buffers. Fewer messages mean an outage fills the buffer more slowly and replays faster; see store and forward for MQTT.
  • Historians and storage. Change-based data compresses well and still reconstructs the signal within the deadband.
  • Broker quotas. Message rate and connection limits apply on every broker. On the public test area, for example, each client may send 10 messages per second.

RBE is usually implemented in an IIoT edge gateway, next to the protocol drivers. For the bigger architectural picture, see the industrial IoT with MQTT guide.

Common Mistakes

  1. No heartbeat. Consumers cannot tell a flat value from a dead gateway.
  2. Comparing with the previous sample instead of the last published value, letting drift through unnoticed.
  3. Deadbands on discrete signals. A state change from 1 to 2 must never be filtered.
  4. Not retaining values. New subscribers see nothing until the next change.
  5. One deadband for every tag. Tune per tag, based on accuracy and consumer needs.

Publish Smarter with ConnectMQ

RBE reduces what you send; a reliable broker makes sure what you send arrives. ConnectMQ is a managed MQTT broker with retained messages, MQTT 5, TLS and a live topic explorer for checking per-tag traffic, and you can test the example above on the free public MQTT broker first. When you are ready, create a free Serverless broker at app.connectmq.com, no credit card required.

Frequently Asked Questions

What is report by exception in simple terms?

It means sending a value only when it changes meaningfully, rather than on every read. A deadband defines "meaningfully" and a heartbeat ensures a value is still sent occasionally.

Does report by exception lose data?

It discards changes smaller than the deadband by design. Choose deadbands based on sensor accuracy and what consumers actually need, and keep raw data locally if engineers may need full resolution later.

What is a good heartbeat interval?

Common choices range from one to fifteen minutes. It should be shorter than the time after which a consumer would treat a value as stale.

Should alarms use report by exception?

Yes, with a zero deadband and no minimum interval, so every transition is published immediately.

Related articles

IIoT Edge Gateway: What It Does and How to Choose One

An IIoT edge gateway connects machines that speak Modbus, OPC UA or proprietary protocols to MQTT and the cloud. Learn its core roles, common architectures and how to choose the right one.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.