MQTT on a Microcontroller
In this article we'll talk about MQTT in a non-conventional way. Why does it exist at all ? Why is it popular ? If you're about to implement a device management system, is MQTT the best fit for you or there are better alternatives?
Why MQTT?
One of the original and most important reasons MQTT became the de-facto protocol for IoT is its ability to connect and control devices that are not directly reachable over the internet. In real networks - homes, offices, factories, cellular networks - devices typically sit behind routers, NAT gateways, or firewalls. These barriers block incoming connections, which makes traditional client/server communication impractical:
Incoming connections are blocked:
However, even the most restrictive firewalls usually allow outgoing TCP
connections:
MQTT takes advantage of this: instead of requiring the cloud or the user to initiate a connection into the device, the device initiates an outbound connection to a publicly - visible MQTT broker. Once this outbound connection is established, the broker becomes a communication hub, enabling control, telemetry, and messaging in both directions:
This simple idea - devices connect out, servers never connect in - solves one of the hardest networking problems in IoT: how to reach devices that you cannot address directly.
To summarise,
- The device opens a long-lived outbound TCP connection to the broker.
- Firewalls/NAT allow outbound connections, and they maintain the state.
- The broker becomes the "rendezvous point" accessible to all.
- The server or user publishes messages to the broker; the device receives them over its already-open connection.
Publish / subscribe
Every MQTT message is carried inside a binary frame with a very small header - typically only a few bytes. These headers contain a command code (called a control packet type) that defines the semantic meaning of the frame. MQTT defines only a handful of these commands, including:
- CONNECT - the client initiates a session with the broker
- PUBLISH - sends a message to a named topic
- SUBSCRIBE - registers interest in one or more topics
- PINGREQ / PINGRESP - keepalive messages to maintain the connection
- DISCONNECT - ends the session cleanly
Because the headers are small and fixed in structure, parsing them on a microcontroller is fast and predictable. The payload that follows these headers can be arbitrary data, from sensor readings to structured messages.
So the publish / subscribe pattern works like this:
a device publishes a message to a topic (a string such as factory/line1/temp).
Other devices subscribe to topics they care about.
The broker delivers messages to all subscribers of each topic.
This model decouples senders and receivers in three important ways:
- In time - publishers and subscribers do not need to be online simultaneously
- In space - devices never need to know each other's IP addresses
- In message flow - many-to-many communication is natural and scalable
For small IoT devices, the pub/sub model removes networking complexity while enabling structured, flexible communication. Combined with MQTT’s minimal framing overhead, it achieves reliable messaging even on low-bandwidth or intermittent links.
Request / response over MQTT
MQTT was originally designed as a broadcast-style protocol, where devices publish telemetry to shared topics and any number of subscribers can listen. This publish/subscribe model is ideal for sensor networks, dashboards, and large-scale IoT systems where data fan-out is needed. However, MQTT can also support more traditional request/response interactions - similar to calling an API - by using a simple topic-based convention.
To implement request/response, each device is assigned two unique topics, typically embedding the device ID:
Request topic (RX): devices/DEVICE_ID/rx - used by the server or controller to send a command to the device.
Response topic (TX): devices/DEVICE_ID/tx - used by the device to send results back to the requester.
When the device receives a message on its RX topic, it interprets the payload as a command, performs the corresponding action, and publishes the response on its TX topic. Because MQTT connections are persistent and outbound from the device, this pattern works even for devices behind NAT or firewalls.
This structure effectively recreates a lightweight RPC-style workflow over MQTT. The controller sends a request to a specific device’s RX topic; the device executes the task and publishes a response to its TX topic. The simplicity of topic naming allows the system to scale cleanly to thousands or millions of devices while maintaining separation and addressing.
With it, it is easy to implement remote device control using MQTT. One of the practical choices is to use JSON-RPC for the request/response.
Security
MQTT includes basic authentication features such as username/password and TLS
encryption, but the protocol itself offers very limited isolation between
clients. Once a client is authenticated, it can typically subscribe to wildcard
topics (e.g., #) and receive all messages published on the broker. Also, it
can publish to any topic, potentially interfering with other devices.
Because MQTT does not define fine-grained access control in its standard, many vendors implement non-standard extensions to ensure proper security boundaries. For example, AWS IoT attaches per-client ACLs tied to X.509 certificates, restricting exactly which topics a device may publish or subscribe to. Similar policy frameworks exist in EMQX, HiveMQ, and other enterprise brokers.
In practice, production systems must rely on these vendor-specific mechanisms to enforce strong authorization and prevent devices from accessing each other’s data.
Implementing on a microcontroller
Microcontrollers are ideal MQTT clients because the protocol is lightweight and designed for low-bandwidth, low-RAM environments. Implementing MQTT on an MCU typically involves integrating three components: a TCP/IP stack (Wi-Fi, Ethernet, or cellular), an MQTT library, and application logic that handles commands and telemetry. After establishing a network connection, the device opens a persistent outbound TCP session to an MQTT broker and exchanges MQTT frames - CONNECT, PUBLISH, SUBSCRIBE - using only a few kilobytes of memory. Most implementations follow an event-driven model: the device subscribes to its command topic, publishes telemetry periodically, and maintains the connection with periodic ping messages. With this structure, even small microcontrollers can participate reliably in large-scale IoT systems.
An example of a fully functional but tiny MQTT client can be found in the Mongoose repository: mqtt-client
Websocket server: an alternative
If all you need is a clean way for your devices to talk to your backend, MQTT can feel like bringing a whole toolbox just to tighten one screw. JSON-RPC over WebSocket keeps things minimal: devices open a WebSocket, send tiny JSON-RPC method calls, and get direct responses. No brokers, no topic trees, no QoS semantics to wrangle.
The really nice part is how naturally this fits into a modern backend. The same service handling the WebSocket connections can also expose a familiar REST API. That REST layer becomes the human- and script-friendly interface, while JSON-RPC over WebSocket stays as the fast “device side” protocol. The backend basically acts as a bridge: REST in, RPC out. This gives you all the advantages of REST - a massive ecosystem of tools, gateways, auth systems, monitoring, and automation - without forcing your devices to speak it.
This setup also avoids one of MQTT’s classic security footguns, where a single authenticated client can accidentally gain visibility or access to messages from the entire fleet just by subscribing to the wrong topic pattern. With a REST/WebSocket bridge, every device connection is isolated, and authentication happens through well-understood web mechanisms like JWTs, mTLS, API keys, OAuth, or whatever your infrastructure already supports. It’s a much more natural fit for modern access control models.
And this isn’t just a neat idea on paper - it’s exactly how vcon.io, Cesanta’s public service for remote device control and firmware updates, is built. Devices talk JSON-RPC over WebSocket. Developers and CI systems talk REST. The backend sits in the middle translating one world into the other. Simple, predictable, and easy to secure.