background-shape
MQTT 101, Pub/Sub for the Edge
August 3, 2022 · 5 min read · by Muhammad Amal programming

TL;DR — MQTT is a publish/subscribe protocol over TCP for low-overhead messaging. Topics are slash-separated strings (factory/line1/temp); wildcards subscribe to subtrees. QoS 0/1/2 trade reliability for overhead. Retained messages give late subscribers the last value. Will messages let dead clients announce death.

After why IIoT, the protocol that everything else in this month builds on. MQTT is simple by design — small spec, low overhead, runs on a Raspberry Pi or a microcontroller. This post is the conceptual map.

The model

MQTT is pub/sub:

  • Clients publish messages to topics
  • Other clients subscribe to topics
  • Broker routes messages from publishers to subscribers
  • No direct client-to-client communication

Topics are strings, slash-separated:

factory/line1/temperature
factory/line1/pressure
factory/line2/temperature
home/livingroom/light/state

Subscribers can use wildcards:

factory/line1/+     → factory/line1/temperature, factory/line1/pressure
factory/+/+         → all line1, line2, etc. with one sub-key
factory/#           → everything under factory/ (any depth)

+ = single level. # = multi-level, only at end.

Message payload is bytes. The broker doesn’t care about format; that’s the application’s job.

QoS levels — reliability tradeoff

Three quality-of-service levels:

QoS 0 — at most once. Fire and forget. No ACK. Fastest, cheapest. Can lose messages on network blips.

QoS 1 — at least once. Publisher waits for PUBACK from broker. Broker re-sends to subscribers until ACKed. Can deliver duplicates.

QoS 2 — exactly once. Four-way handshake. Guarantees no loss, no duplicates. Slowest, most overhead.

In practice for IIoT:

  • QoS 0: high-frequency sensor data where you can afford to lose individual readings (e.g., 100 Hz vibration data — the next sample is moments away)
  • QoS 1: most common. State changes, commands, alarms.
  • QoS 2: rarely. Financial transactions or things that genuinely need exact-once.

QoS is per-message and per-subscription. Publisher decides the publish QoS; subscriber decides the receive QoS. The lower of the two wins.

Retained messages

A retained message is stored by the broker. New subscribers get it immediately on subscribe, without waiting for the publisher to send another one.

PUBLISH factory/line1/door/state ON  retain=true
# Subscriber connects after this — gets ON immediately

Use for:

  • Current state (machine status, valve position, door open/closed)
  • Configuration that should be available on connection
  • Last-known-value semantics

Don’t use for:

  • Event streams (you want each subscriber to see the live stream from connect-time)
  • Time-sensitive data (“the temperature was X 5 minutes ago” doesn’t mean it’s X now)

Will messages (last will and testament)

A “will” is a message the broker publishes if the client disconnects ungracefully (network loss, crash):

CONNECT will_topic="factory/line1/heartbeat" will_payload="offline" will_qos=1 will_retain=true

If this client dies, the broker publishes offline to factory/line1/heartbeat. Other subscribers know it’s down.

Pattern: every device publishes online to its heartbeat topic on connect (retained); sets will to publish offline on disconnect (retained). Anyone subscribing to factory/+/heartbeat sees real-time device status.

MQTT 5 features worth knowing

MQTT 5 (2018, widely supported by 2022):

  • User properties — key/value metadata per message (like HTTP headers)
  • Reason codes — granular error info on ACKs
  • Topic aliases — short numeric IDs for frequently-used topics, reduces bandwidth
  • Message expiry — server-side TTL
  • Subscription identifiers — distinguish multiple subs from one client

For new IIoT projects in 2022, use MQTT 5. For brownfield (existing devices speak MQTT 3.1.1), it’s fine to coexist — brokers handle both.

How it differs from Kafka, AMQP, NATS

MQTT vs Kafka:

  • MQTT is push to clients; Kafka is pull (clients poll)
  • MQTT has no persistent log by default; Kafka is log-first
  • MQTT is single-message routing; Kafka is partition-based
  • MQTT runs on constrained devices; Kafka requires JVM
  • MQTT for device → cloud; Kafka for cloud → cloud

Often paired: edge devices speak MQTT → bridge → Kafka → consumers.

MQTT vs AMQP (RabbitMQ):

  • AMQP has exchanges + routing keys + queues; MQTT has topics
  • AMQP is heavier (more bytes per message)
  • AMQP has broader feature set (transactions, dead-letter, priorities)
  • MQTT scales better for many clients (1M+ connections to one broker)

MQTT vs NATS:

  • Similar message size and overhead
  • NATS has stronger built-in persistence story (JetStream)
  • MQTT has better mindshare in OT/IIoT
  • Both work; depends on existing ecosystem

For IIoT: MQTT. For pure cloud event streaming: probably Kafka or NATS. Hybrid: MQTT at edge, Kafka in cloud, bridge between.

Topic naming conventions

There’s no enforced schema. Conventions that work:

<area>/<location>/<device>/<measurement>
factory/line1/press42/temperature
factory/line1/press42/pressure
factory/line2/conveyor3/speed

Some prefer reverse-hierarchy for grouping:

<account>/<site>/<device>/<topic>
acme/jakarta/sensor-1234/vibration

Pick one; document it; stick with it. Topic names are de facto API — changing them breaks every subscriber.

Connecting from code

JavaScript (Node.js):

import mqtt from 'mqtt';
const client = mqtt.connect('mqtts://broker.example.com:8883', {
  username: 'user',
  password: 'pass',
});

client.on('connect', () => {
  client.subscribe('factory/line1/+');
});
client.on('message', (topic, payload) => {
  console.log(`${topic}: ${payload.toString()}`);
});
client.publish('factory/line1/door/state', 'open', { qos: 1, retain: true });

Python (paho-mqtt):

import paho.mqtt.client as mqtt

def on_message(client, userdata, msg):
    print(f"{msg.topic}: {msg.payload}")

client = mqtt.Client()
client.username_pw_set("user", "pass")
client.tls_set()
client.on_message = on_message
client.connect("broker.example.com", 8883)
client.subscribe("factory/line1/+")
client.loop_forever()

Go (paho.mqtt.golang):

import mqtt "github.com/eclipse/paho.mqtt.golang"

opts := mqtt.NewClientOptions().
    AddBroker("tls://broker.example.com:8883").
    SetUsername("user").
    SetPassword("pass")
client := mqtt.NewClient(opts)
token := client.Connect()
token.Wait()
client.Subscribe("factory/line1/+", 1, func(c mqtt.Client, m mqtt.Message) {
    fmt.Printf("%s: %s\n", m.Topic(), m.Payload())
})

All three sit on top of the wire protocol. Choose by what language your application is in.

Common Pitfalls

No structured topic naming. Six months in, every subscriber needs to know 200 ad-hoc topic strings.

QoS 2 everywhere. Throughput tanks. Most messages don’t need exact-once.

Retained messages for event streams. New subscribers get stale data, confused. Retained for state only.

Will messages misconfigured. Will fires even on planned disconnects unless you DISCONNECT cleanly. Tune to expected behavior.

No client IDs or duplicate client IDs. Two clients with the same ID; broker disconnects the first when second connects. Stable, unique IDs per device.

Wrapping Up

MQTT is small, sufficient, and the right protocol for edge → cloud telemetry. Friday: broker comparison — picking which to actually run.