Show code
import {InlineKnowledgeCheck} from "../assets/js/iotclass-inline-kc.js"
MQTT’s binary packet format uses a compact 2-byte minimum fixed header with variable-length encoding to minimize overhead on constrained networks. This chapter covers packet structure internals, scalable topic hierarchy design patterns, bandwidth optimization techniques, and MQTT 5.0 features like message expiry, topic aliases, and shared subscriptions.
Learning Objectives
By the end of this chapter, you will be able to:
Analyze MQTT Packet Structure : Decode binary MQTT packets field by field and calculate exact byte overhead for given topic lengths and QoS levels
Design Topic Hierarchies : Construct scalable, maintainable topic naming conventions that support wildcard queries for large device fleets
Calculate Bandwidth Savings : Apply optimization techniques — short topic names, binary payloads, topic aliases — and quantify savings across a device fleet
Implement MQTT 5.0 Features : Configure message expiry, topic aliases, and shared subscriptions in code and explain when each feature is appropriate
Evaluate Broker Options : Compare Mosquitto, EMQX, HiveMQ, and cloud brokers and justify selection based on device count, message throughput, and cost
Diagnose Capacity Requirements : Assess connection counts, RAM needs, and bandwidth demand for a production IoT deployment
Topic : UTF-8 string hierarchy (e.g., sensors/building-A/room-101/temperature) routing messages to subscribers
Topic Level : Segment between / separators — each level represents a dimension of the topic hierarchy
Single-Level Wildcard (+) : Matches exactly one topic level: sensors/+/temperature matches sensors/room1/temperature
Multi-Level Wildcard (#) : Matches remaining levels: sensors/# matches all topics starting with sensors/
Retained Message : Last message stored per topic — new subscribers immediately receive current state on subscription
Topic Hierarchy Design : Best practice: device-type/device-id/measurement enables fine-grained subscription filtering
$SYS Topics : Reserved broker system topics (e.g., $SYS/broker/clients/connected) publishing broker statistics
For Beginners: MQTT Advanced Concepts
Beyond basic publish-subscribe, MQTT offers powerful features for building robust IoT systems. Retained messages store the last value so new subscribers get data immediately. Last will messages announce when a device goes offline. These features turn simple messaging into a reliable IoT communication platform.
“I just learned about retained messages and they’re amazing!” exclaimed Sammy the Sensor. “When I publish my temperature with the retain flag, the broker remembers it. So when a new phone app connects at midnight, it instantly sees ‘22 degrees’ instead of waiting until my next reading.”
Lila the LED shared her discovery: “And I set up a Last Will message! When I connect to the broker, I say: ‘If I ever disconnect unexpectedly, tell everyone that Lila is offline.’ So if the power goes out, the monitoring system knows immediately – even though I can’t send messages anymore because I’m off!”
“My favorite,” said Bella the Battery, “is clean session = false . When I go to sleep to save power, the broker holds all messages that arrive while I’m napping. When I wake up, I get everything I missed – like checking your text messages after airplane mode. Nothing gets lost during my power naps!”
Max the Microcontroller summed up: “These aren’t just nice extras – they solve real problems. Retained messages prevent stale data. Last will detects failures. Persistent sessions handle intermittent connections. That’s why MQTT runs billions of IoT devices worldwide!”
Prerequisites
Before diving into this chapter, you should be familiar with:
MQTT Packet Structure
Understanding MQTT’s binary packet format is essential for protocol debugging and optimization.
Remaining Length Encoding
MQTT uses variable-length encoding to minimize overhead:
0 - 127
1 byte
23 -> 0x17
128 - 16,383
2 bytes
200 -> 0xC8 0x01
16,384 - 2,097,151
3 bytes
50,000 -> 0xD0 0x86 0x03
2,097,152 - 268,435,455
4 bytes
1,000,000 -> 0xC0 0x84 0x3D
PUBLISH Packet Example
Topic: "sensor/temp"
Payload: "25.5"
QoS: 1
Hex dump:
32 13 00 0B 73 65 6E 73 6F 72 2F 74 65 6D 70 00 01 32 35 2E 35
Breakdown:
32 -> Fixed header: PUBLISH (0011), DUP=0, QoS=01, RETAIN=0
(0x32 = 0011 0010: bits 7-4 = message type 3, bit 3 = DUP=0, bits 2-1 = QoS 1, bit 0 = RETAIN=0)
13 -> Remaining length: 19 bytes
(2 topic-length + 11 topic + 2 packet-ID + 4 payload = 19 = 0x13)
00 0B -> Topic length: 11 bytes
73 65 6E 73 6F 72 2F 74 65 6D 70 -> "sensor/temp" (UTF-8)
00 01 -> Packet ID: 1 (required for QoS 1 acknowledgment)
32 35 2E 35 -> "25.5" (UTF-8 payload)
Control Packet Types
1
CONNECT
Client->Broker
Connection request
2
CONNACK
Broker->Client
Connection acknowledgment
3
PUBLISH
Both
Publish message
4
PUBACK
Both
QoS 1 acknowledgment
5
PUBREC
Both
QoS 2 step 1
6
PUBREL
Both
QoS 2 step 2
7
PUBCOMP
Both
QoS 2 step 3
8
SUBSCRIBE
Client->Broker
Subscribe to topics
9
SUBACK
Broker->Client
Subscribe acknowledgment
12
PINGREQ
Client->Broker
Keep-alive ping
13
PINGRESP
Broker->Client
Ping response
14
DISCONNECT
Client->Broker
Graceful disconnect
Show code
viewof calcTopicLength = Inputs. range ([1 , 100 ], {
label : "Topic length (bytes)" ,
value : 15 ,
step : 1
})
viewof calcPayloadSize = Inputs. range ([1 , 500 ], {
label : "Payload size (bytes)" ,
value : 20 ,
step : 1
})
viewof qosLevel = Inputs. select ([0 , 1 , 2 ], {
label : "QoS level" ,
value : 1
})
viewof retainFlag = Inputs. toggle ({
label : "Retained message" ,
value : false
})
Show code
fixedHeader = 2
topicLengthField = 2
packetIdSize = (qosLevel > 0 ) ? 2 : 0
totalPacketSize = fixedHeader + topicLengthField + calcTopicLength + packetIdSize + calcPayloadSize
overheadSize = fixedHeader + topicLengthField + calcTopicLength + packetIdSize
overheadPercent = ((overheadSize / totalPacketSize) * 100 ). toFixed (1 )
html `<div style="background: linear-gradient(135deg, #3498DB 0%, #9B59B6 100%); padding: 24px; border-radius: 8px; color: white; margin-top: 16px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid #E67E22; padding-bottom: 8px;">MQTT Packet Size Calculator</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-top: 16px;">
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px;">
<div style="font-size: 0.85em; opacity: 0.9;">Fixed Header</div>
<div style="font-size: 1.3em; font-weight: bold;"> ${ fixedHeader} bytes</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px;">
<div style="font-size: 0.85em; opacity: 0.9;">Topic Length Field</div>
<div style="font-size: 1.3em; font-weight: bold;"> ${ topicLengthField} bytes</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px;">
<div style="font-size: 0.85em; opacity: 0.9;">Topic String</div>
<div style="font-size: 1.3em; font-weight: bold;"> ${ calcTopicLength} bytes</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px;">
<div style="font-size: 0.85em; opacity: 0.9;">Packet ID (QoS ${ qosLevel} )</div>
<div style="font-size: 1.3em; font-weight: bold;"> ${ packetIdSize} bytes</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px;">
<div style="font-size: 0.85em; opacity: 0.9;">Payload</div>
<div style="font-size: 1.3em; font-weight: bold;"> ${ calcPayloadSize} bytes</div>
</div>
</div>
<div style="margin-top: 20px; padding: 16px; background: rgba(255,255,255,0.15); border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div>
<div style="font-size: 0.9em; opacity: 0.9;">Total Packet Size</div>
<div style="font-size: 2em; font-weight: bold;"> ${ totalPacketSize} bytes</div>
</div>
<div>
<div style="font-size: 0.9em; opacity: 0.9;">Protocol Overhead</div>
<div style="font-size: 2em; font-weight: bold;"> ${ overheadSize} bytes ( ${ overheadPercent} %)</div>
</div>
</div>
</div>
<div style="margin-top: 12px; padding: 10px; background: rgba(255,255,255,0.05); border-radius: 6px; font-size: 0.85em;">
<strong>Configuration:</strong> QoS ${ qosLevel} , Retain= ${ retainFlag ? "Yes" : "No" }
</div>
</div>`
Packet Size Optimization
Keep topic names short - h/l/t vs home/living_room/temperature saves 20 bytes
Use binary payloads - 0x19 (1 byte) vs "25" (2 bytes)
Choose appropriate QoS - QoS 0 uses 50% less messages than QoS 1
Limit retained messages - Only essential status topics
Increase keep-alive interval - 300s vs 60s = 80% fewer PINGREQ packets
Example savings:
Topic: h/b/t (5 bytes) vs home/bedroom/temperature (23 bytes) = 18 bytes saved
100 messages/day x 365 days = 657 KB saved per year per device
For 1000 devices = 641 MB saved annually
Every MQTT message includes the full topic string. Shorter topics save bandwidth:
Message size with topic: $ S_{} = S_{} + S_{} + S_{} $
Where: - \(S_{\text{fixed}}\) = 4 bytes (MQTT fixed header + topic length field) - \(S_{\text{topic}}\) = topic string length in bytes - \(S_{\text{payload}}\) = payload size
Long vs short topic comparison (100 messages/day, 1 year):
Long topic: building/floor3/room305/sensors/temperature (42 bytes) $ S_{} = 4 + 42 + 10 = 56 $
Short topic: b/3/305/t (8 bytes) $ S_{} = 4 + 8 + 10 = 22 $
Savings per message: \(56 - 22 = 34\text{ bytes}\) (61% reduction)
Annual bandwidth savings (1000 sensors @ 100 msg/day): $ = 1000 = 1.16 $
Energy savings (cellular @ 8 mA TX, 1 byte = 32 μs @ 250 kbps): $ E_{} = 34 = 8.7 $
Over a year: \(36,500\text{ msgs} \times 8.7\text{ μAs} = 317.6\text{ mAs} \approx 0.088\text{ mAh}\)
Cellular data cost savings ($0.10/MB): \(1.16\text{ GB} = 1{,}188\text{ MB}\)
\(1{,}188\text{ MB} \times \$0.10\text{/MB} = \$118.80\text{ per year for fleet}\)
Lesson: Topic naming conventions have real operational costs. Short, hierarchical topics save bandwidth, energy, and money at scale.
Show code
viewof longTopicLength = Inputs. range ([10 , 100 ], {
label : "Long topic length (bytes)" ,
value : 42 ,
step : 1
})
viewof shortTopicLength = Inputs. range ([5 , 50 ], {
label : "Short topic length (bytes)" ,
value : 8 ,
step : 1
})
viewof payloadSize = Inputs. range ([1 , 200 ], {
label : "Payload size (bytes)" ,
value : 10 ,
step : 1
})
viewof topicCalcMessagesPerDay = Inputs. range ([1 , 1000 ], {
label : "Messages per day" ,
value : 100 ,
step : 10
})
viewof deviceCount = Inputs. range ([1 , 10000 ], {
label : "Number of devices" ,
value : 1000 ,
step : 100
})
viewof cellularCostPerMB = Inputs. range ([0.01 , 1.0 ], {
label : "Cellular cost ($/MB)" ,
value : 0.10 ,
step : 0.01
})
Show code
topicCalcFixedHeaderSize = 4
topicCalcLongMessageSize = topicCalcFixedHeaderSize + longTopicLength + payloadSize
topicCalcShortMessageSize = topicCalcFixedHeaderSize + shortTopicLength + payloadSize
topicCalcSavingsPerMessage = topicCalcLongMessageSize - topicCalcShortMessageSize
topicCalcReductionPercent = ((topicCalcSavingsPerMessage / topicCalcLongMessageSize) * 100 ). toFixed (1 )
topicCalcMessagesPerYear = topicCalcMessagesPerDay * 365
topicCalcTotalDeviceMessages = topicCalcMessagesPerYear * deviceCount
topicCalcBandwidthSavedBytes = topicCalcTotalDeviceMessages * topicCalcSavingsPerMessage
topicCalcBandwidthSavedGB = (topicCalcBandwidthSavedBytes / (1024 * 1024 * 1024 )). toFixed (2 )
topicCalcAnnualCost = (topicCalcBandwidthSavedGB * cellularCostPerMB). toFixed (2 )
html `<div style="background: linear-gradient(135deg, #2C3E50 0%, #16A085 100%); padding: 24px; border-radius: 8px; color: white; margin-top: 16px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid #E67E22; padding-bottom: 8px;">Topic Name Bandwidth Calculator</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 16px;">
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 0.85em; opacity: 0.9;">Long Message Size</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ topicCalcLongMessageSize} bytes</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #16A085;">
<div style="font-size: 0.85em; opacity: 0.9;">Short Message Size</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ topicCalcShortMessageSize} bytes</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #3498DB;">
<div style="font-size: 0.85em; opacity: 0.9;">Savings Per Message</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ topicCalcSavingsPerMessage} bytes ( ${ topicCalcReductionPercent} %)</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #9B59B6;">
<div style="font-size: 0.85em; opacity: 0.9;">Annual Bandwidth Saved</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ topicCalcBandwidthSavedGB} GB</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E74C3C;">
<div style="font-size: 0.85em; opacity: 0.9;">Annual Cost Savings</div>
<div style="font-size: 1.5em; font-weight: bold;">$ ${ topicCalcAnnualCost} </div>
</div>
</div>
<div style="margin-top: 16px; padding: 12px; background: rgba(255,255,255,0.05); border-radius: 6px; font-size: 0.9em;">
<strong>Fleet Impact:</strong> ${ deviceCount. toLocaleString ()} devices × ${ topicCalcMessagesPerDay} msg/day × 365 days = ${ topicCalcTotalDeviceMessages. toLocaleString ()} messages/year
</div>
</div>`
Worked Example: Smart Building Topic Design
Designing Topic Hierarchy for Smart Building
Scenario : Design the MQTT topic structure for a 10-story commercial office building. Each floor has 20 rooms with temperature sensors, occupancy detectors, and smart lighting.
Step 1: Identify Requirements
Telemetry : Temperature, humidity, occupancy from 200 rooms
Commands : Control lights, blinds, HVAC per room
Status : Online/offline for 600+ devices
Alerts : Fire alarms, security events
Access patterns : Dashboard shows all temps, HVAC controls one floor
Step 2: Design Base Structure
Bad approach (flat topics):
sensor_floor1_room101_temp # No hierarchy, can't use wildcards
sensor_floor1_room101_humidity # 600+ individual subscriptions!
Good approach (hierarchical):
building/floor1/room101/sensors/temperature
building/floor1/room101/sensors/humidity
building/floor1/room101/lights/status
building/floor1/room101/lights/command
Step 3: Apply Naming Conventions
{building}/{floor}/{room}/{device_type}/{measurement_or_action}
Examples:
building/floor03/room305/sensors/temperature # Telemetry
building/floor03/room305/lights/command # Control
building/floor03/room305/lights/status # State
building/floor03/hvac/setpoint # Zone control
building/alerts/fire # Building-wide
Naming rules:
Use lowercase with no spaces
Use / as separator only
Pad numbers for sorting: floor03, not floor3
End with action type: /temperature, /command, /status
Step 4: Plan Wildcard Subscriptions
All temps (dashboard)
building/+/+/sensors/temperature
200 topics
One floor’s sensors
building/floor05/+/sensors/#
40 topics
One room’s everything
building/floor03/room305/#
~10 topics
All alerts
building/alerts/#
Fire, security
Step 5: Handle Edge Cases
# Shared spaces (no room number)
building/floor01/lobby/sensors/occupancy
building/stairwell-a/sensors/smoke
# Building-wide systems
building/hvac/chiller/status
building/elevator/car1/position
building/energy/meter/consumption
# System topics
$SYS/broker/clients/connected
building/$status/gateway/floor03
Result: Topic Hierarchy
Key design decisions:
Physical hierarchy (building/floor/room) enables location-based queries
Device type grouping (sensors, lights) separates telemetry from control
Action suffixes (status, command) distinguish read vs write
Padded numbers (floor03) ensure correct sorting
Worked Example: Fleet Tracking Topics
Scenario : 500 delivery trucks with GPS, fuel level, and engine temperature sensors. Dispatch needs individual truck queries and aggregate fleet data.
Step 1: Define topic structure
fleet/{truck_id}/{sensor_type}
Examples:
fleet/truck-001/gps
fleet/truck-001/fuel
fleet/truck-001/temp
Step 2: Enable efficient queries
All data from truck-001
fleet/truck-001/#
Single subscription
All GPS data
fleet/+/gps
Cross-fleet GPS
All data
fleet/#
Fleet dashboard
Step 3: Add metadata topics
fleet/truck-001/status # online/offline (retained)
fleet/truck-001/location/city # Current city (retained)
fleet/alerts/breakdown # Fleet-wide alerts
MQTT 5.0 Features
MQTT 5.0 introduced significant enhancements for enterprise IoT:
Message Expiry (TTL)
# MQTT 5.0: Message expires after 60 seconds
publish_properties = Properties(PacketTypes.PUBLISH)
publish_properties.MessageExpiryInterval = 60 # seconds
client.publish(
"sensors/temperature" ,
"25.5" ,
qos= 1 ,
properties= publish_properties
)
# If subscriber is offline > 60 seconds, message is discarded
Use case: Sensor readings that become stale quickly (GPS, real-time status).
Topic Aliases (Bandwidth Optimization)
# First message: Establish alias
publish_properties = Properties(PacketTypes.PUBLISH)
publish_properties.TopicAlias = 1
client.publish(
"building/floor03/room305/sensors/temperature" , # 42 bytes
"25.5" ,
properties= publish_properties
)
# Subsequent messages: Use alias only
publish_properties.TopicAlias = 1
client.publish(
"" , # Empty topic, use alias (saves 42 bytes!)
"25.6" ,
properties= publish_properties
)
Savings: 1000 messages/hour with 40-byte topics = 40 KB/hour saved per device.
Show code
viewof aliasTopicLength = Inputs. range ([10 , 100 ], {
label : "Topic name length (bytes)" ,
value : 42 ,
step : 1
})
viewof aliasPayloadSize = Inputs. range ([1 , 100 ], {
label : "Payload size (bytes)" ,
value : 10 ,
step : 1
})
viewof messagesPerHour = Inputs. range ([10 , 10000 ], {
label : "Messages per hour" ,
value : 1000 ,
step : 100
})
viewof hoursPerDay = Inputs. range ([1 , 24 ], {
label : "Operating hours per day" ,
value : 24 ,
step : 1
})
Show code
aliasSize = 2
aliasFirstMessageSize = 4 + aliasTopicLength + aliasPayloadSize + aliasSize
aliasSubsequentMessageSize = 4 + 0 + aliasPayloadSize + aliasSize
aliasSavingsPerMessage = aliasFirstMessageSize - aliasSubsequentMessageSize
aliasMessagesPerDay = messagesPerHour * hoursPerDay
dailySavingsBytes = (aliasMessagesPerDay - 1 ) * aliasSavingsPerMessage
dailySavingsKB = (dailySavingsBytes / 1024 ). toFixed (2 )
annualSavingsBytes = dailySavingsBytes * 365
annualSavingsMB = (annualSavingsBytes / (1024 * 1024 )). toFixed (2 )
html `<div style="background: linear-gradient(135deg, #16A085 0%, #3498DB 100%); padding: 24px; border-radius: 8px; color: white; margin-top: 16px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid #E67E22; padding-bottom: 8px;">MQTT 5.0 Topic Alias Savings Calculator</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 16px;">
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 0.85em; opacity: 0.9;">First Message Size</div>
<div style="font-size: 1.4em; font-weight: bold;"> ${ aliasFirstMessageSize} bytes</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">With full topic name</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #16A085;">
<div style="font-size: 0.85em; opacity: 0.9;">Subsequent Messages</div>
<div style="font-size: 1.4em; font-weight: bold;"> ${ aliasSubsequentMessageSize} bytes</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Using alias only</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #3498DB;">
<div style="font-size: 0.85em; opacity: 0.9;">Savings Per Message</div>
<div style="font-size: 1.4em; font-weight: bold;"> ${ aliasSavingsPerMessage} bytes</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Topic eliminated</div>
</div>
</div>
<div style="margin-top: 20px; padding: 16px; background: rgba(255,255,255,0.15); border-radius: 6px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div style="border-left: 4px solid #9B59B6; padding-left: 12px;">
<div style="font-size: 0.9em; opacity: 0.9;">Daily Savings</div>
<div style="font-size: 1.8em; font-weight: bold;"> ${ dailySavingsKB} KB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;"> ${ aliasMessagesPerDay. toLocaleString ()} messages/day</div>
</div>
<div style="border-left: 4px solid #E74C3C; padding-left: 12px;">
<div style="font-size: 0.9em; opacity: 0.9;">Annual Savings</div>
<div style="font-size: 1.8em; font-weight: bold;"> ${ annualSavingsMB} MB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Per device per year</div>
</div>
</div>
</div>
<div style="margin-top: 12px; padding: 10px; background: rgba(255,255,255,0.05); border-radius: 6px; font-size: 0.85em;">
<strong>Note:</strong> Topic alias established once, then reused for all subsequent messages in the session
</div>
</div>`
Shared Subscriptions (Load Balancing)
# Three workers share subscription to same topic
Worker-1: SUBSCRIBE "$share/workers/sensors/temperature"
Worker-2: SUBSCRIBE "$share/workers/sensors/temperature"
Worker-3: SUBSCRIBE "$share/workers/sensors/temperature"
# Broker distributes messages round-robin:
Message 1 -> Worker-1
Message 2 -> Worker-2
Message 3 -> Worker-3
Message 4 -> Worker-1 (cycles)
Request/Response Pattern
# Requester: Send command with response topic
request_props = Properties(PacketTypes.PUBLISH)
request_props.ResponseTopic = "devices/sensor001/response"
request_props.CorrelationData = b"request-123"
client.publish(
"devices/sensor001/command" ,
'{"cmd": "get_config"}' ,
properties= request_props
)
# Responder: Reply to specified topic
def on_message(client, userdata, msg):
cmd = json.loads(msg.payload)
if cmd["cmd" ] == "get_config" :
response_props = Properties(PacketTypes.PUBLISH)
response_props.CorrelationData = msg.properties.CorrelationData
client.publish(
msg.properties.ResponseTopic,
'{"interval": 60, "qos": 1}' ,
properties= response_props
)
Feature Comparison
Message expiry
Not supported
Built-in TTL
Reason codes
1 (success/fail)
256 detailed codes
User properties
Encode in payload
Native support
Topic aliases
Not supported
Up to 65535 aliases
Shared subscriptions
Broker-specific
Standardized
Flow control
Not supported
Built-in
Recommendation: Use MQTT 5.0 for new projects. Fall back to 3.1.1 only for legacy compatibility.
Broker Selection
Popular MQTT Brokers
Mosquitto
Free, open-source
DIY, Raspberry Pi
~100K
EMQX
Open-source
Large scale
10M+
HiveMQ
Commercial
Enterprise, clustering
10M+
AWS IoT Core
Cloud
AWS integration
Unlimited
test.mosquitto.org
Public test
Learning only
N/A
Selection Criteria
< 1,000 devices
Mosquitto (simple, free)
1,000 - 100,000
EMQX or VerneMQ
> 100,000
HiveMQ, AWS IoT Core
Multi-cloud
Self-hosted cluster
Show code
viewof brokerDevices = Inputs. range ([100 , 100000 ], {
label : "Number of devices" ,
value : 50000 ,
step : 1000
})
viewof brokerMsgPerHour = Inputs. range ([1 , 100 ], {
label : "Messages per device per hour" ,
value : 4 ,
step : 1
})
viewof brokerAvgMsgSize = Inputs. range ([50 , 500 ], {
label : "Average message size (bytes)" ,
value : 150 ,
step : 10
})
viewof brokerQoS = Inputs. select ([0 , 1 , 2 ], {
label : "Typical QoS level" ,
value : 1
})
Show code
totalMessagesPerHour = brokerDevices * brokerMsgPerHour
messagesPerSecond = (totalMessagesPerHour / 3600 ). toFixed (1 )
peakMessagesPerSecond = (brokerDevices / 900 ). toFixed (0 )
ramPerConnection = 20
totalConnectionRAM = ((brokerDevices * ramPerConnection) / 1024 ). toFixed (1 )
messageQueueRAM = 2.0
totalRAM = (parseFloat (totalConnectionRAM) + messageQueueRAM). toFixed (1 )
bandwidthBytesPerSecond = totalMessagesPerHour * brokerAvgMsgSize / 3600
bandwidthMbps = ((bandwidthBytesPerSecond * 8 ) / (1024 * 1024 )). toFixed (2 )
dailyStorageBytes = totalMessagesPerHour * 24 * brokerAvgMsgSize
dailyStorageMB = (dailyStorageBytes / (1024 * 1024 )). toFixed (0 )
recommendedBroker = brokerDevices < 1000 ? "Mosquitto" :
brokerDevices < 100000 ? "EMQX or VerneMQ" :
"HiveMQ or AWS IoT Core"
html `<div style="background: linear-gradient(135deg, #9B59B6 0%, #E74C3C 100%); padding: 24px; border-radius: 8px; color: white; margin-top: 16px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid #E67E22; padding-bottom: 8px;">MQTT Broker Capacity Planner</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-top: 16px;">
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 0.85em; opacity: 0.9;">Concurrent Connections</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ brokerDevices. toLocaleString ()} </div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #16A085;">
<div style="font-size: 0.85em; opacity: 0.9;">Message Rate</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ messagesPerSecond} msg/sec</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Average sustained</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #3498DB;">
<div style="font-size: 0.85em; opacity: 0.9;">Peak Burst Rate</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ peakMessagesPerSecond} msg/sec</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">15-min boundary</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #9B59B6;">
<div style="font-size: 0.85em; opacity: 0.9;">RAM Required</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ totalRAM} GB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Sessions + queue</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E74C3C;">
<div style="font-size: 0.85em; opacity: 0.9;">Network Bandwidth</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ bandwidthMbps} Mbps</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Sustained throughput</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #7F8C8D;">
<div style="font-size: 0.85em; opacity: 0.9;">Daily Storage (QoS ${ brokerQoS} )</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ dailyStorageMB} MB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">24-hour retention</div>
</div>
</div>
<div style="margin-top: 20px; padding: 16px; background: rgba(255,255,255,0.2); border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 1em; font-weight: bold; margin-bottom: 8px;">Recommended Broker</div>
<div style="font-size: 1.6em; font-weight: bold;"> ${ recommendedBroker} </div>
<div style="font-size: 0.85em; margin-top: 8px; opacity: 0.9;">
For ${ brokerDevices. toLocaleString ()} devices @ ${ brokerMsgPerHour} msg/hour each
</div>
</div>
</div>`
Show code
viewof retainedTopics = Inputs. range ([100 , 100000 ], {
label : "Number of retained topics" ,
value : 10000 ,
step : 500
})
viewof retainedPayloadSize = Inputs. range ([10 , 1000 ], {
label : "Average payload size (bytes)" ,
value : 100 ,
step : 10
})
viewof retainedUpdateInterval = Inputs. select (
["1 second" , "1 minute" , "15 minutes" , "1 hour" , "1 day" ],
{
label : "Typical update interval" ,
value : "1 hour"
}
)
viewof newSubscribersPerHour = Inputs. range ([0 , 1000 ], {
label : "New subscribers per hour" ,
value : 50 ,
step : 10
})
Show code
_updateSecondsLookup = ({
"1 second" : 1 ,
"1 minute" : 60 ,
"15 minutes" : 900 ,
"1 hour" : 3600 ,
"1 day" : 86400
})
updateSeconds = _updateSecondsLookup[retainedUpdateInterval]
retainedStorageKB = ((retainedTopics * retainedPayloadSize) / 1024 ). toFixed (1 )
retainedStorageMB = ((retainedTopics * retainedPayloadSize) / (1024 * 1024 )). toFixed (2 )
updatesPerDay = Math . floor (86400 / updateSeconds)
dailyUpdateBytes = retainedTopics * retainedPayloadSize * updatesPerDay
dailyUpdateMB = (dailyUpdateBytes / (1024 * 1024 )). toFixed (1 )
bandwidthSavedPerSubscriber = retainedTopics * retainedPayloadSize
hourlySavingsKB = ((bandwidthSavedPerSubscriber * newSubscribersPerHour) / 1024 ). toFixed (1 )
dailySavingsMB = ((hourlySavingsKB * 24 ) / 1024 ). toFixed (1 )
avgWaitTime = (updateSeconds / 2 ). toFixed (0 )
avgWaitDisplay = avgWaitTime >= 3600 ? ` ${ (avgWaitTime / 3600 ). toFixed (1 )} hours` :
avgWaitTime >= 60 ? ` ${ (avgWaitTime / 60 ). toFixed (0 )} minutes` :
` ${ avgWaitTime} seconds`
html `<div style="background: linear-gradient(135deg, #E67E22 0%, #2C3E50 100%); padding: 24px; border-radius: 8px; color: white; margin-top: 16px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid #16A085; padding-bottom: 8px;">Retained Message Storage & Savings Calculator</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 16px;">
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 0.85em; opacity: 0.9;">Broker Storage</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ retainedStorageMB} MB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;"> ${ retainedTopics. toLocaleString ()} topics</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #2C3E50;">
<div style="font-size: 0.85em; opacity: 0.9;">Daily Update Traffic</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ dailyUpdateMB} MB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;"> ${ updatesPerDay. toLocaleString ()} updates/day</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #16A085;">
<div style="font-size: 0.85em; opacity: 0.9;">Hourly Bandwidth Saved</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ hourlySavingsKB} KB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;"> ${ newSubscribersPerHour} new subscribers</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #3498DB;">
<div style="font-size: 0.85em; opacity: 0.9;">Daily Bandwidth Saved</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ dailySavingsMB} MB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">From instant delivery</div>
</div>
</div>
<div style="margin-top: 20px; padding: 16px; background: rgba(255,255,255,0.15); border-radius: 6px;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div style="border-left: 4px solid #9B59B6; padding-left: 12px;">
<div style="font-size: 0.9em; opacity: 0.9;">Avg Wait Without Retain</div>
<div style="font-size: 1.8em; font-weight: bold;"> ${ avgWaitDisplay} </div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Until next update</div>
</div>
<div style="border-left: 4px solid #E74C3C; padding-left: 12px;">
<div style="font-size: 0.9em; opacity: 0.9;">With Retained Messages</div>
<div style="font-size: 1.8em; font-weight: bold;">Instant</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;">Last known value</div>
</div>
</div>
</div>
<div style="margin-top: 12px; padding: 10px; background: rgba(255,255,255,0.05); border-radius: 6px; font-size: 0.85em;">
<strong>Use Case:</strong> Device status monitoring, dashboard displays, mobile apps connecting intermittently. Each new subscriber gets ${ retainedTopics. toLocaleString ()} last values instantly instead of waiting ${ retainedUpdateInterval} .
</div>
</div>`
Worked Example: MQTT Broker Capacity Planning
Scenario: A utility company is deploying 50,000 smart electricity meters across a metropolitan area. Each meter reports consumption every 15 minutes and must receive firmware updates and tariff schedules. Design the MQTT infrastructure.
Step 1: Calculate message rates
Inbound (meters -> broker):
50,000 meters x 4 readings/hour = 200,000 messages/hour
Peak (all meters reporting in same minute): 50,000/15 = 3,333 msg/sec burst
Outbound (broker -> meters):
Tariff updates: 50,000 meters x 1 update/day = 2,083 msg/hour
Firmware: 500 meters/night x 200 chunks = 100,000 msg/night (batched)
Total sustained: ~205,000 messages/hour = 57 messages/second average
Peak: 3,333 messages/second (15-minute boundary)
Step 2: Size the broker
Connections
50,000 persistent + 50 admin + 10 analytics
50,060 concurrent
RAM per connection
~20 KB (session state + subscriptions)
1.0 GB for sessions
Message queue RAM
QoS 1 requires store-and-forward
2.0 GB for inflight
Network bandwidth
3,333 msg/sec x 150 bytes avg = 488 KB/sec peak
4 Mbps sustained
Disk (persistent messages)
200,000 msg/hr x 150 bytes x 24 hrs
720 MB/day retention
Step 3: Select broker and topology
EMQX cluster
3 nodes x 8 vCPU, 16 GB RAM
1,800 EUR (self-hosted)
Open source, full control, needs DevOps
HiveMQ Cloud
Managed, auto-scaling
3,200 EUR
Zero ops, SLA guaranteed, vendor lock-in
AWS IoT Core
Serverless, pay-per-message
4,100 EUR (at 205K msg/hr)
No infrastructure, but 0.08 USD/million messages adds up
Decision: EMQX cluster selected. At 50,000 devices, self-hosted saves 1,400-2,300 EUR/month vs. managed alternatives. Break-even for managed services is below ~15,000 devices where DevOps overhead exceeds subscription cost.
Step 4: Topic structure for operations
utility/{region}/{meter_id}/reading # QoS 0, every 15 min
utility/{region}/{meter_id}/alert # QoS 1, tamper/outage events
utility/{region}/{meter_id}/command # QoS 1, tariff updates
utility/{region}/{meter_id}/firmware # QoS 1, OTA chunks
utility/{region}/{meter_id}/status # QoS 0, retained, online/offline
Operations subscriptions:
utility/north/+/alert -> NOC dashboard (region-filtered)
utility/+/+/reading -> Analytics pipeline (all readings)
$SYS/broker/# -> Monitoring (broker health)
Monitoring thresholds:
Message queue depth
> 10,000
> 50,000
Connection rate
> 500/sec
> 1,000/sec (possible reconnect storm)
Publish latency (p99)
> 100 ms
> 500 ms
Retained message count
> 100,000
> 200,000
Concept Check
Show code
InlineKnowledgeCheck ({
containerId : "kc-mqtt-advanced-1" ,
question : "A smart city deploys 50,000 streetlights publishing status every 5 minutes. Each light uses topic `city/zone1/light123/status`. A traffic management system needs to subscribe to all lights in zone1. Which topic hierarchy design would enable the MOST efficient subscription?" ,
options : [
"Current design `city/zone1/light123/status` - subscribe with `city/zone1/+/status`" ,
"Flat topics `light123_zone1_status` - subscribe to each individually" ,
"Single topic `city/all_lights` with light ID in payload" ,
"Reversed hierarchy `status/zone1/light123` - subscribe with `status/zone1/#`"
],
correctIndex : 0 ,
explanation : "The hierarchical design `city/zone1/light123/status` with subscription `city/zone1/+/status` is most efficient. The `+` wildcard matches one level (light ID), enabling a single subscription to receive all 50,000 lights' status updates. Flat topics require 50,000 individual subscriptions. Single topic with payload parsing wastes broker resources filtering at application layer. Reversed hierarchy works but doesn't follow logical physical→measurement organization." ,
difficulty : "intermediate"
})
Show code
InlineKnowledgeCheck ({
containerId : "kc-mqtt-advanced-2" ,
question : "An agricultural IoT system uses MQTT 5.0 topic aliases to reduce bandwidth. The first message sends topic `farm/field1/soil/moisture` (25 bytes) with alias=1. How many bytes are saved per message for 1,000 subsequent readings?" ,
options : [
"25 KB total - topic replaced with 2-byte alias" ,
"23 KB total - topic (25 bytes) minus alias (2 bytes) = 23 bytes saved per message" ,
"25 KB total - entire topic name eliminated" ,
"No savings - topic aliases only work for QoS 2"
],
correctIndex : 1 ,
explanation : "Topic aliases save 23 bytes per message after the first. The first message sends the full 25-byte topic and establishes alias=1. Subsequent messages use only the 2-byte alias, saving 25 - 2 = 23 bytes per message. Over 1,000 messages: 23 bytes × 1,000 = 23,000 bytes = 23 KB saved. This is particularly valuable for long hierarchical topics in bandwidth-constrained scenarios like LoRaWAN or satellite links. Topic aliases work with all QoS levels, not just QoS 2." ,
difficulty : "advanced"
})
Concept Relationships
Advanced MQTT features connect to both protocol internals and system architecture:
MQTT Protocol Layers:
Advanced Features:
MQTT 5.0 Specification - Official standard
Shared Subscriptions - Load balancing pattern
Message Expiry - Time-to-live for stale data
Topic Aliases - Bandwidth optimization
Broker Technologies:
Mosquitto - Single-node, learning deployments
EMQX - Clustering for 10M+ connections
HiveMQ - Enterprise with managed clustering
VerneMQ - Distributed Erlang-based broker
System Integration:
Message Broker Clustering - HA patterns
Load Balancing - Connection distribution
Edge Gateway Design - Topic bridging
Capacity Planning - Sizing brokers
Prerequisites You Should Know:
MQTT packet structure: 2-byte fixed header + variable header + payload
Variable-length encoding saves bytes for small messages
Broker memory: ~10-20 KB per connection + message queue storage
Topic hierarchy depth impacts wildcard matching performance
What This Enables:
Optimize bandwidth usage with topic aliases (23 bytes saved per message)
Design scalable topic hierarchies supporting wildcard queries
Plan broker capacity: connections, message throughput, memory
Select appropriate broker for deployment scale (100K vs 10M devices)
See Also
MQTT Protocol Internals:
Broker Comparison:
Topic Design Patterns:
MQTT 5.0 Features:
Implementation Guides:
Try It Yourself
Experiment 1: MQTT Packet Structure Analysis
Capture and analyze MQTT packets with Wireshark:
# Install mosquitto broker and clients
sudo apt install mosquitto mosquitto-clients
# Start Wireshark with MQTT filter
wireshark -f "tcp port 1883" -k
# In another terminal, publish a message
mosquitto_pub -h localhost -t "test/topic" -m "Hello MQTT" -q 1
What to Observe:
Fixed header: 2 bytes (0x32 for PUBLISH QoS 1)
Variable header: topic length (2 bytes) + topic (10 bytes) + packet ID (2 bytes)
Payload: “Hello MQTT” (10 bytes)
Total: 26 bytes (minimal overhead!)
Experiment 2: Topic Hierarchy Performance
Compare wildcard matching efficiency:
import paho.mqtt.client as mqtt
import time
# Flat topics (inefficient)
flat_topics = [f"sensor_ { i} _temperature" for i in range (1000 )]
# Hierarchical topics (efficient)
hierarchical_topics = [f"building/floor { i// 100 } /room { i% 100 } /temp" for i in range (1000 )]
# Measure subscription time
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.connect ("localhost" , 1883 )
start = time.time()
for topic in flat_topics:
client.subscribe(topic, qos= 0 )
flat_time = time.time() - start
# Clear subscriptions
client.disconnect()
client.connect ("localhost" , 1883 )
start = time.time()
client.subscribe("building/+/+/temp" , qos= 0 ) # Single wildcard subscription!
hierarchical_time = time.time() - start
print (f"Flat (1000 subscriptions): { flat_time:.3f} s" )
print (f"Hierarchical (1 wildcard): { hierarchical_time:.3f} s" )
print (f"Speedup: { flat_time/ hierarchical_time:.0f} x faster" )
What to Observe:
Flat: ~0.5-1.0 seconds for 1,000 subscriptions
Hierarchical: ~0.001 seconds for 1 wildcard
500-1000x faster subscription setup!
Experiment 3: MQTT 5.0 Topic Aliases
Measure bandwidth savings with topic aliases (requires MQTT 5.0 broker):
from paho.mqtt.client import Client as MQTTClient, MQTTv5
from paho.mqtt.properties import Properties
from paho.mqtt.packettypes import PacketTypes
client = MQTTClient(callback_api_version= 2 , protocol= MQTTv5)
client.connect ("localhost" , 1883 )
# First message: establish alias
long_topic = "farm/northfield/zone1/row12/plant45/soil/moisture"
props = Properties(PacketTypes.PUBLISH)
props.TopicAlias = 1
client.publish(long_topic, "25.5" , properties= props)
print (f"First message: { len (long_topic)} byte topic" )
# Subsequent messages: use alias (empty topic)
for i in range (100 ):
props = Properties(PacketTypes.PUBLISH)
props.TopicAlias = 1
client.publish("" , f"2 { i} . { i} " , properties= props) # Empty topic, uses alias!
savings = len (long_topic) * 100 # Bytes saved over 100 messages
print (f"Saved { savings} bytes with topic alias" )
What to Observe:
Topic name: 50 bytes
100 messages: saves 50 × 100 = 5,000 bytes
Critical for LoRaWAN (200 byte/day limit)
Challenge: Broker Capacity Planning
Calculate broker requirements for a smart city deployment:
Given:
- 100,000 streetlights
- Publish every 5 minutes (12 msg/hour each)
- 3 subscribers per message (dashboard, analytics, alerts)
- Average message: 120 bytes
Calculate:
1. Messages per hour
2. Broker fan-out factor
3. Required bandwidth
4. RAM for connections (assume 20 KB per connection)
5. Select appropriate broker (Mosquitto, EMQX, or HiveMQ)
Bonus: Build your own capacity planning calculator!
Show code
viewof costDevices = Inputs. range ([100 , 100000 ], {
label : "Number of devices" ,
value : 10000 ,
step : 1000
})
viewof costMsgPerDay = Inputs. range ([1 , 1000 ], {
label : "Messages per device per day" ,
value : 100 ,
step : 10
})
viewof costAvgSize = Inputs. range ([50 , 1000 ], {
label : "Average message size (bytes)" ,
value : 200 ,
step : 10
})
viewof costDeployment = Inputs. select (["Cellular (per MB)" , "AWS IoT Core (per million)" , "Cloud MQTT (monthly flat)" ], {
label : "Deployment type" ,
value : "Cellular (per MB)"
})
viewof costCellularPerMB = Inputs. range ([0.01 , 1.0 ], {
label : "Cellular cost ($/MB)" ,
value : 0.10 ,
step : 0.01
})
viewof costAwsPerMillion = Inputs. range ([0.5 , 5.0 ], {
label : "AWS IoT Core ($/million messages)" ,
value : 1.0 ,
step : 0.1
})
viewof costCloudMonthly = Inputs. range ([10 , 1000 ], {
label : "Cloud MQTT monthly ($)" ,
value : 99 ,
step : 10
})
Show code
totalMessagesPerDay = costDevices * costMsgPerDay
totalMessagesPerMonth = totalMessagesPerDay * 30
totalBytesPerMonth = totalMessagesPerMonth * costAvgSize
totalMBPerMonth = (totalBytesPerMonth / (1024 * 1024 )). toFixed (2 )
totalGBPerMonth = (totalBytesPerMonth / (1024 * 1024 * 1024 )). toFixed (2 )
cellularCost = (totalMBPerMonth * costCellularPerMB). toFixed (2 )
awsCost = ((totalMessagesPerMonth / 1000000 ) * costAwsPerMillion). toFixed (2 )
cloudCost = costCloudMonthly. toFixed (2 )
selectedCost = costDeployment === "Cellular (per MB)" ? cellularCost :
costDeployment === "AWS IoT Core (per million)" ? awsCost :
cloudCost
deployAnnualCost = (selectedCost * 12 ). toFixed (2 )
costPerDevice = (selectedCost / costDevices). toFixed (4 )
html `<div style="background: linear-gradient(135deg, #E74C3C 0%, #2C3E50 100%); padding: 24px; border-radius: 8px; color: white; margin-top: 16px; margin-bottom: 24px;">
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid #E67E22; padding-bottom: 8px;">MQTT Deployment Cost Calculator</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 16px;">
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 0.85em; opacity: 0.9;">Total Devices</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ costDevices. toLocaleString ()} </div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #16A085;">
<div style="font-size: 0.85em; opacity: 0.9;">Messages/Month</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ (totalMessagesPerMonth / 1000000 ). toFixed (2 )} M</div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 12px; border-radius: 6px; border-left: 4px solid #3498DB;">
<div style="font-size: 0.85em; opacity: 0.9;">Data Volume/Month</div>
<div style="font-size: 1.5em; font-weight: bold;"> ${ totalGBPerMonth} GB</div>
<div style="font-size: 0.75em; margin-top: 4px; opacity: 0.8;"> ${ totalMBPerMonth} MB</div>
</div>
</div>
<div style="margin-top: 20px; padding: 20px; background: rgba(255,255,255,0.2); border-radius: 6px; border-left: 4px solid #E67E22;">
<div style="font-size: 0.9em; opacity: 0.9; margin-bottom: 4px;">Deployment: ${ costDeployment} </div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 12px;">
<div>
<div style="font-size: 0.9em; opacity: 0.9;">Monthly Cost</div>
<div style="font-size: 2.2em; font-weight: bold;">$ ${ selectedCost} </div>
</div>
<div>
<div style="font-size: 0.9em; opacity: 0.9;">Annual Cost</div>
<div style="font-size: 2.2em; font-weight: bold;">$ ${ deployAnnualCost} </div>
</div>
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,0.2);">
<div style="font-size: 0.85em; opacity: 0.9;">Cost Per Device</div>
<div style="font-size: 1.3em; font-weight: bold;">$ ${ costPerDevice} /month</div>
</div>
</div>
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; overflow-x: auto;">
<div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 0.8em; opacity: 0.8;">Cellular</div>
<div style="font-size: 1.2em; font-weight: bold;">$ ${ cellularCost} </div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 0.8em; opacity: 0.8;">AWS IoT Core</div>
<div style="font-size: 1.2em; font-weight: bold;">$ ${ awsCost} </div>
</div>
<div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 6px; text-align: center;">
<div style="font-size: 0.8em; opacity: 0.8;">Cloud MQTT</div>
<div style="font-size: 1.2em; font-weight: bold;">$ ${ cloudCost} </div>
</div>
</div>
</div>`
Summary
Key takeaways:
MQTT packets have minimal overhead (2-byte header minimum)
Topic hierarchy enables powerful wildcard queries
MQTT 5.0 adds message expiry, topic aliases, and shared subscriptions
Choose broker based on scale and feature requirements
At 50,000+ devices, self-hosted brokers save 40-55% vs. managed/serverless options
Topic design principles:
Use hierarchical structure for wildcard queries
Physical-then-logical organization
Consistent naming conventions
Plan for future scalability
What’s Next
Now that you understand MQTT’s advanced features, continue with:
MQTT Practice and Exercises
Hands-on exercises and common pitfalls
Apply packet analysis and topic design in guided scenarios
MQTT Comprehensive Review
Broker internals and message flow
Deepen understanding of how brokers route and store messages at scale
MQTT Labs
ESP32 implementation and real hardware
Build working MQTT clients and integrate sensors with a live broker
MQTT Security
TLS, authentication, and access control
Secure your broker and understand how encryption adds overhead
MQTT QoS Levels
Acknowledgment flows and session state
Understand the packet exchanges underpinning QoS 1 and QoS 2
CoAP Protocol
REST-style IoT protocol over UDP
Compare with MQTT and choose the right protocol for your use case