I have been organizing books I read before, and picked up Building an IoT Platform from 0 to 1 again.

This book helped me a lot, especially when my company adopted MQTT. What I needed back then wasn't another "connect to a broker in five minutes" demo — the internet is full of those — but to know where the platform would lose control once a large fleet of devices actually showed up.
After the devices connect
My initial mental model was a straight line: devices connect and publish, the backend subscribes, data gets written to the DB. If the local demo received messages, it looked like half the work was done.
Real devices are not like that. They go offline, reconnect, switch networks, and may wake up for only a few seconds at a time because of battery or signal. What the platform faces is not one stable request but a crowd of clients, each carrying an old session, old firmware, and a clock that isn't quite right. "Can it connect right now" is only the entrance question. The harder ones are what happened while it was offline, which data is still valid after it reconnects, and whether the state shown in the UI is a fact, a guess, or an observation that expired a long time ago.
What MQTT gives you, and what EMQX gives you
This is a habit I picked up from this book: keep the protocol and the implementation in separate mental drawers. The OASIS MQTT 5.0 specification defines packets, sessions, QoS — the behavior both sides must follow; EMQX is one broker implementation among several. Mix MQTT protocol capabilities together with EMQX implementation settings in your notes, and your architecture decisions look portable while actually being tied to one product.
Authorization is the classic example. With a large number of clients constantly publishing and subscribing, if every hot path does a synchronous lookup against an external DB, the authorization service itself becomes the bottleneck first. The session authorization cache and node-level external resource cache described in the EMQX authorization docs are implementation choices the broker offers to cut repeated lookups — not guarantees that MQTT 5 hands you automatically.
And a cache is not just about hit rate; it's about invalidation. EMQX's session cache clears on disconnect or reconnect, and you can set a TTL or flush it manually. But what if someone's permission gets revoked? The platform has to define how quickly that must take effect and whether existing connections should be kicked. Skip that decision and the performance win buys you an authorization gap nobody can explain.
And after the PUBACK arrives?
A QoS 1 PUBACK is the transport-level acknowledgment for a PUBLISH on this MQTT hop — it is not application-level proof of completed execution. The broker accepting a message, a downstream consumer receiving it, and the device executing the command are three different boundaries, and you can't collapse them into one "success" state. A PUBACK proves at most that the protocol exchange moved one step forward — whether the motor actually turned, whether the firmware rejected the parameters, whether the device dropped offline right before executing, all of that has to come back from the device as an application event.
Reliable delivery has another face: redelivery. Both the device side and the backend need to dedupe or stay idempotent using the command ID, or one network hiccup turns "delivered at least once" into "physical action performed twice."
Shared Subscription splits traffic
In development there is usually a single subscriber; in production there are several pods digesting telemetry at the same time. If they all use a regular subscription on the same topic, every instance can receive its own copy; you need $share/backend-workers/device/+/telemetry for the group to be treated as one processing pool.
But what the EMQX Shared Subscription docs describe is picking one subscriber in the group per message — load balancing, that's it. It is not end-to-end exactly-once: disconnects, session expiry, redelivery, and consumers dying mid-work still leave the application layer facing duplicates and half-finished state. So database writes, event publishing, and external API calls still need a replayable design — keep a stable identifier for the message or command, block duplicates with a unique key, and acknowledge only after the durable work has actually finished.
The life of a command
"Sent" carries no information for the person operating the system. I split a command into explicit states — accepted, running, succeeded, failed, expired — and record platform acceptance separately from device reports. Every operation gets a command ID that doesn't change across retries, and state transitions must reject late regressions — once the device has reported succeeded, a late-arriving running can't flip the screen back to in-progress, and an expired command shouldn't spring back to life when the device reconnects.
Group operations add one more layer. If a command selects devices by tag, you have to store the tag version or the actual target snapshot at that moment. Otherwise, days later, when someone asks "which devices was this command actually aimed at," all you can do is reinterpret history with tags that have already changed.
Receive Maximum is directional
MQTT 5's Receive Maximum is a directional quota declared by the receiver: it caps how many QoS 1/2 PUBLISHes the other side can have in flight, unacknowledged, at once. What the client declares in CONNECT governs server to client; what the server declares in CONNACK governs client to server. It is not a system-wide throughput knob, and it doesn't apply to QoS 0.
The EMQX inflight and message queue docs describe a different layer: the broker keeps sent-but-unacknowledged messages per client connection under max_inflight, and once that's full, messages go to the queue. This relates to MQTT's Receive Maximum, but the broker's implementation settings also involve its own caps, legacy client behavior, and queue eviction policy — the protocol quota and the EMQX setting are not the same layer of control.
Topic Alias handles a different cost: within one network connection, an alias-to-topic mapping is established first, and the long topic name can be omitted afterwards. It saves packet overhead, but it is not a name table that survives reconnects, and it won't rescue a slow consumer from inflight buildup.
The question that remains: can you trust the state
A trustworthy device state cannot be just online: true. At minimum it should say which event the state came from, the device time versus the platform receive time, which firmware or config version was involved, and whether this observation has aged past acceptable freshness. Late telemetry must not overwrite newer values, and connection status can't stand in for business status — a device being online doesn't mean a command can execute, and the platform not seeing a response doesn't mean the device never received it. That uncertainty belongs in the model, not compressed by the UI into a single green dot.
Protocol choice comes back to constraints too. IETF RFC 7252 positions CoAP as a request/response protocol for constrained nodes on low-power, lossy networks, with basic message exchange over UDP. For some small-memory, brief-wakeup, or Web-style resource scenarios it may fit better than MQTT — but that is a conditional choice, not a blanket "CoAP is lighter, use it for all IoT."
If I were designing a platform today, I'd ask first: how much state can the device hold? How long can the network stay down? Do commands have physical side effects? Is redelivery allowed after a device goes dark? Those answers drive session, QoS, cache, queue, command lifecycle, and protocol — not the other way around, where one broker's feature list gets stamped onto every device.
The biggest thing this book gave me is still that plain shift: the hard part of MQTT is not connecting, because the connection is only the beginning. The real work is making sure that permissions, messages, commands, and late data can pass through real-world interference and the platform can still present a device state you can trust.