MQTT vs CoAP for Constrained IoT Devices: A Head-to-Head on CC2652
MQTT's persistent connections suit devices with stable connectivity. CoAP's stateless request-response suits intermittent links. The CC2652 running Zephyr is a fair test bed.

MQTT wins when you have stable TCP and need pub/sub semantics with persistent sessions. CoAP wins when links are intermittent, UDP is acceptable, and you want request-response without connection state. On a CC2652R1 LaunchPad running Zephyr 3.5.0, we ran both against the same sensor workload to get numbers instead of blog-post opinions.
Test bed
- Hardware: CC2652R1 LP, 2.4 GHz, +5 dBm TX
- RTOS: Zephyr 3.5.0,
west updatepinned tozephyr-v3.5.0 - MQTT: Eclipse Mosquitto 2.0.18 on a Pi 4, TLS off for baseline (added later)
- CoAP: libcoap 4.3.4 via Zephyr
CONFIG_COAP - Payload: 24-byte CBOR struct (temp, humidity, seq, battery mV), 60 s interval
- Link: Wi-Fi backhaul for gateway; device on Thread border router in one run, direct Wi-Fi in another
We measured: bytes on wire per hour, reconnect count after router reboot, and worst-case time-to-first-publish after power cycle.
MQTT results
Persistent session with clean_session=0, QoS 1, keepalive 60 s.
- Steady state: ~890 B/hour overhead (PINGREQ/PINGRESP + small publishes) excluding TLS
- Router reboot: reconnect in 4–12 s; one missed publish window if reboot landed mid-interval
- RAM: ~18 KB for mbedTLS + MQTT client stack when TLS enabled; without TLS, ~11 KB
- CPU: negligible at 60 s interval; TLS handshake on reconnect cost ~400 ms active radio time
MQTT fits gateways that stay powered and talk upstream over Ethernet or reliable Wi-Fi. On Thread-only nodes with a sleepy border router, TCP stalls were painful—30+ s hangs until TCP timeout unless we tuned keepalive aggressively (battery cost).
CoAP results
Observe pattern to a CoAP server, confirmable messages for critical alerts only.
- Steady state: ~620 B/hour at same payload rate (no persistent TCP window)
- Router reboot: next UDP datagram either succeeds or fails fast; app retried in 2 s, no half-open state
- RAM: ~6 KB client footprint
- Block-wise transfer: needed for OTA chunks; added complexity vs MQTT's natural streaming
CoAP shines on lossy links where you cannot afford TCP's retransmission ambiguity. RFC 7252's deduplication window requires care—duplicate detection state is per endpoint pair.
Head-to-head tradeoffs
| Concern | MQTT | CoAP |
|---|---|---|
| Connection state | Yes (TCP + optional session) | Stateless UDP |
| Firewall/NAT | Often works (outbound 8883) | UDP may be blocked |
| Pub/sub native | Yes | Needs CoAP pub-sub draft or app layer |
| TLS cost on MCU | High (mbedTLS) | DTLS similar cost, different failure modes |
| Zephyr maturity | Good (mqtt_lib) | Good, fewer examples |
For our fleet of mains-powered environmental sensors on reliable Wi-Fi, MQTT + TLS remained the default. For battery nodes on Thread with 30 s poll intervals, CoAP observe reduced reconnect churn and saved ~40 µA average by avoiding TCP keepalive.
Scheduling interactions matter—see Zephyr vs FreeRTOS scheduling for why we pinned network work to a dedicated workqueue instead of the system workqueue.
CAN-backed industrial gateways are a different animal entirely; CAN bus node on STM32G0 is where we document that path.
What I'd do next
- Hybrid gateway: MQTT northbound, CoAP southbound on the border router—one codebase, two adapters behind an interface.
- Add DTLS session resumption benchmarks; initial handshake dominated cold-start OTA.
- Formalize link-quality metric (ETX or RSSI smoothed) to flip protocol preference at runtime instead of compile-time
CONFIG_*.
TLS and DTLS overhead on CC2652
When security is non-negotiable, budget radio time explicitly. Full TLS 1.2 handshake on connect: ~12 KB RX, ~8 KB TX over Wi-Fi path in our trace—800 ms wall time dominated by cert chain size, not symmetric crypto. CoAP DTLS 1.2 with PSK cut handshake to 90 ms but introduced key provisioning workflow pain (per-device PSK in factory fixture).
Session resumption (TLS ticket or DTLS connection ID where supported) dropped reconnect after router reboot from 800 ms to 120 ms—worth implementing before optimizing application payload format.
Observability on constrained stacks
Neither stack gives you free metrics. We counted ENOMEM from mbedTLS, CoAP retransmit counters, and MQTT disconnect reason codes into a single uint32 bitmask uplinked hourly. Without that, field "random offline" was un diagnosable.
Thread border router sleep
Sleepy Thread BR dropped MQTT upstream—device queue filled, CoAP observe subscription expired silently. Added application-level heartbeat independent of transport; protocol choice does not replace liveness design.
Payload format neutrality
Switching CBOR → protobuf saved 8 B per message—not enough to change MQTT vs CoAP decision at 60 s interval. Optimize payload after picking transport; religious protobuf debates wasted one sprint.
NAT timeout tuning
Corporate firewall UDP 5683 idle timeout 30 s broke CoAP observe on customer site—MQTT TCP 8883 survived. Site survey checklist now includes middlebox timeout for chosen protocol before install sign-off.
Pick the protocol that matches your link reliability model, not your cloud vendor's SDK defaults.
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

