Why MQTT for device fleets
A vending machine on a 2G fallback connection cannot afford HTTP’s handshake overhead per reading. MQTT keeps one lightweight persistent connection, publishes on kilobyte-scale topics, and — critically — the broker knows the moment a device disconnects. That connection-state awareness, via keep-alive and Last Will messages, is what turns a fleet dashboard from a guess into a fact.
Topic design as API design
- Hierarchical and predictable:
vending/{machineId}/telemetry/stock,ev/{chargerId}/status— wildcards let services subscribe to exactly their slice. - Devices publish narrow, subscribe narrower: a machine sees only its own command topic — per-device ACLs enforced at the broker.
- Retained messages for last-known state: a dashboard connecting at 3 a.m. gets every device’s status instantly, no polling.
- Version topics from day one:
v1/vending/...— 2000 field devices do not upgrade on your schedule.
QoS, honestly applied
- QoS 0 for high-frequency telemetry — losing one temperature reading of 60 per hour is nothing.
- QoS 1 for transactions and commands — delivery guaranteed, consumers deduplicate (the vending sale must arrive; it must not arrive twice as two sales).
- QoS 2 almost never — the handshake cost rarely justifies it when idempotent consumers make QoS 1 sufficient.
- Last Will on every device: broker publishes
offlinethe moment a machine vanishes — field-service dispatch runs on this signal.
Worked example: device contract and platform subscriber
A vending machine’s complete topic contract, and the platform side subscribing with MQTTnet — note the Last Will doing the fleet-status heavy lifting:
# Device publishes QoS
v1/vending/VM-2041/telemetry/stock 0 every 15 min
v1/vending/VM-2041/transaction 1 on each vend
v1/vending/VM-2041/status 1 retained: "online"
# Broker publishes on unclean disconnect (Last Will)
v1/vending/VM-2041/status 1 retained: "offline"
# Device subscribes (its slice only, enforced by ACL)
v1/vending/VM-2041/command/#
// Platform subscriber
var opts = new MqttClientOptionsBuilder()
.WithTcpServer("broker.internal", 8883).WithTlsOptions(o => o.Build())
.WithCredentials("ingest-svc", secret)
.Build();
await client.SubscribeAsync("v1/vending/+/transaction", MqttQualityOfServiceLevel.AtLeastOnce);
client.ApplicationMessageReceivedAsync += e =>
_ingest.EnqueueAsync(e.ApplicationMessage.Topic, e.ApplicationMessage.Payload);
FAQ
MQTT for anything with a persistent device population: lower overhead, real connection state, and server-push commands. HTTP still fine for firmware downloads and bulk uploads over the side channel.
EMQX and Mosquitto have both served me well — EMQX for large fleets needing clustering and per-device auth at scale, Mosquitto for lean single-node deployments. Managed options (AWS IoT Core) trade control for zero ops.
OCPP runs over WebSocket, not MQTT — but the fleet-management lessons (connection state, offline queuing, command patterns) transfer directly. My platforms run both: OCPP for chargers, MQTT for vending and sensor fleets.