MQTT Explained: Why AI Agents Need an Event Bus

MQTT & AI Systems, episode 1. Video: MQTT Explained: Why AI Agents Need an Event Bus

An AI agent tells an operator: "Line 3 is at 71 °C. All good." The sensor went offline forty minutes ago. 71 was the last thing it ever said.

The agent wasn't wrong. It was reading a bus that never told it the sensor had died. That's the problem MQTT solves, as long as you use the two features most tutorials skip. This post covers the protocol from the agent's side: what it can know, when it can trust it, and where each feature has a trap.

Table of Contents

Send the verdict, not the video

Think about a camera above a production line, or a car on a motorway. Neither can wait for a round trip to the cloud, and neither can push every video frame up the network without choking it.

So the model runs next to the camera, and what travels isn't the video. It's the verdict, a few hundred bytes:

{ "defect": true, "camera": 2, "confidence": 0.94 }

To be precise about the car: it brakes on its own internal network, and that decision never leaves the vehicle. What leaves is the event, for the fleet, the plant or the agent. And the protocol built for small messages over unreliable links, on one outbound connection, is MQTT.

Why not poll an HTTP API?

Picture 500 sensors and an agent polling each one every second (illustrative numbers). That's 500 requests a second, and most answers are "nothing changed". Polling scales with the number of readers: add a second agent and the traffic doubles.

Publish/subscribe turns that around. A sensor speaks only when something changes, once, to a broker. The broker hands a copy to every interested reader, so the sensor's link carries each change once however many agents listen. The broker's outbound traffic still grows with subscribers, but that's one server you size, not 500 devices. (Same argument for databases: Stop Polling Your Database.)

Broker, client, topic

Three words:

  • Broker: the one server in the middle.
  • Clients: cameras, sensors and agents. They connect to the broker, and only to it. They never talk to each other.
  • Topic: a path, with nothing to declare up front. plant/line3/cam2/inference/defect.

The camera publishes its verdict to that topic, and anyone subscribed to it gets the message. The camera doesn't know who is listening. Add a second agent tomorrow and the camera never changes. That decoupling is the whole point.

In .NET with MQTTnet 5, both sides fit on a screen:

using MQTTnet;
using MQTTnet.Formatter;
using MQTTnet.Protocol;

var factory = new MqttClientFactory();

// The agent: subscribes to a filter, not a single topic.
using var agent = factory.CreateMqttClient();
agent.ApplicationMessageReceivedAsync += e =>
{
    Console.WriteLine($"{e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}");
    return Task.CompletedTask;
};
await agent.ConnectAsync(new MqttClientOptionsBuilder()
    .WithTcpServer("localhost", 1883)
    .WithProtocolVersion(MqttProtocolVersion.V500)
    .Build());
await agent.SubscribeAsync(factory.CreateSubscribeOptionsBuilder()
    .WithTopicFilter("plant/+/+/inference/defect", MqttQualityOfServiceLevel.AtLeastOnce)
    .Build());

// The camera: publishes a verdict and knows nothing about who reads it.
using var camera = factory.CreateMqttClient();
await camera.ConnectAsync(new MqttClientOptionsBuilder()
    .WithTcpServer("localhost", 1883)
    .WithProtocolVersion(MqttProtocolVersion.V500)
    .Build());
await camera.PublishAsync(new MqttApplicationMessageBuilder()
    .WithTopic("plant/line3/cam2/inference/defect")
    .WithPayload("""{ "defect": true, "camera": 2, "confidence": 0.94 }""")
    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
    .Build());

Wildcards: a subscription is a filter

Subscribers don't have to name one topic. They filter the tree:

Filter Matches
plant/+/+/inference/defect every defect, on every line, from every camera. + is exactly one level
plant/line3/# everything under line 3. # is every level below

If you know RabbitMQ, it's the topic exchange idea, with + in place of *. So a subscription isn't a queue name, it's a filter on the tree. That means the shape of your topic tree decides what your agent can ask for, so design it on purpose.

QoS 0, 1 and 2

What if the network drops? The car just entered a tunnel. MQTT has three delivery levels:

  • QoS 0, at most once: fire and forget. Fine for a temperature every second, because the next reading replaces a lost one.
  • QoS 1, at least once: the receiver acknowledges with PUBACK. When the car leaves the tunnel and resumes its session, it resends everything the broker never acknowledged ([MQTT-4.4.0-1]). That means duplicates: your handler must be idempotent, or the agent raises the same defect alarm twice (what happens when a message is processed twice).
  • QoS 2, exactly once, for one hop: four packets (PUBLISH, PUBREC, PUBREL, PUBCOMP), more latency, and still not end to end.

QoS is also per hop. A subscriber granted QoS 0 receives a QoS 1 message at QoS 0 ([MQTT-3.8.4-8]).

My rule: telemetry at 0, detections at 1, and design for the duplicate.

Retained messages: the agent's memory

A new agent starts up. What does it know? Nothing, until the camera speaks again.

A retained message fixes that. Publish with the retain flag and the broker keeps the last message on that topic, one per topic, and hands it to every new subscriber the moment it subscribes. Retained is the agent's memory of the current state. Not history, only the latest value.

And here's the trap from the opening. If the sensor loses power, the broker still holds 71 °C. A new agent subscribes, gets 71, and has no hint that it's stale.

MQTT 5 gives you a first guard: message expiry. Set it, and the broker discards the retained value once it expires.

Last Will: presence is a topic

So how does the broker tell you a client died? With the Last Will.

When a client connects, it hands the broker a message to publish on its behalf if the connection drops without a clean goodbye. Camera 2 connects with a Will on its status topic, payload offline, retained, and then publishes online, also retained, on the same topic.

Power cut. No goodbye. After one and a half keep-alive periods of silence, the broker gives up on the connection ([MQTT-3.1.2-22]) and publishes offline for it. A new agent now subscribes to both the value and the status. If the status says offline, 71 means nothing.

Presence is a topic too. That's the fix for the opening story, and it isn't instant. I measured exactly how long it takes for a crash compared with a power cut in the next post: MQTT Retained Messages and Last Will Explained (with a .NET Demo).

Sessions and shared subscriptions

Two more features before the agent.

Persistent sessions. The broker queues messages while a client is away, but the queue is bounded. On Azure Event Grid, for example, it's 100 messages or 1 MB, and then the session is terminated. Know your broker's number.

Shared subscriptions. Normally every subscriber gets its own copy. Prefix the filter with $share/<group>/, for example $share/agents/plant/#, and the agents in that group split the messages between them: competing consumers.

One catch: a shared subscription never receives retained messages (MQTT 5.0 §4.8.2). An agent that only subscribes through $share never sees the current state. Current state and shared work need two different subscriptions.

Where the agent plugs in

Put it together:

  1. At the edge, a small model runs beside the camera and publishes verdicts to a broker on site, so the local loop stays in milliseconds.
  2. In the cloud, an agent subscribes: retained topics for what it knows, the status topic for whether to trust it.
  3. To act, it publishes to a command topic. Senses are retained, hands are publishes.

Every client opens one outbound connection, so there are no inbound firewall holes. The site broker is bridged to the cloud, and only the topics the cloud needs cross over. Some brokers, like EMQX, even let an agent call tools through topics, so ordinary topic access rules limit what it can do.

One caution: an agent with unrestricted access is a risk. Narrow tools, narrow topics.

Which broker?

Broker Typical use
Mosquitto local and small
EMQX, HiveMQ scale
Azure IoT Operations on site, at the edge
Azure Event Grid in the cloud

Same protocol, different limits. Event Grid's broker, for example, supports QoS 0 and 1 only, with a 1 MB message cap (limits page). Read the limits page before you design around a feature.

Key takeaways

  • Send the verdict, not the video. MQTT carries small events over unreliable links on one outbound connection.
  • Topics decouple. Publishers never know their readers, and subscriptions are filters on the tree.
  • QoS 1 means duplicates. Make handlers idempotent; QoS 2 is exactly once for one hop only.
  • Retained is current state, and it goes stale silently. Expiry and the Last Will tell you when it's dead.
  • Shared subscriptions skip retained messages. Use a second subscription for current state.

MQTT is the agent's senses and hands, and both lie unless you design for presence, expiry and duplicates.

Quick test: what does a new agent receive from a sensor that just lost power? Answer in the video comments, or read the next episode on retained messages and Last Will.

Sources