MQTT vs HTTP for IoT telemetry: bytes, battery and commands
Most fleets choose between two ways to get readings off a device. MQTT is publish/subscribe: the device holds one TLS connection open to a broker, publishes each reading on a topic, and receives commands over the same connection. HTTP, usually a REST endpoint that takes JSON, is request/response: the device POSTs a reading and waits for a status code, and the server can answer but never start a conversation. Both work. They differ where a prototype on a desk never looks: the bytes a cellular SIM pays for, battery life, how commands reach the device, and how fast anyone notices it has gone quiet. This guide compares them on each, with packet sizes counted from the specs and handshakes measured on our servers in October 2026.
Bytes per reading, counted
Take one reading in Synacl's payload format, a 66-byte JSON body: {"ts":1792411200000,"values":{"temperature":23.4,"humidity":61.2}}. Over MQTT it goes on a 70-byte topic, tenants/<24-character id>/devices/<24-character id>/data; over HTTP, in a POST to the webhook endpoint.
| Transport | Device sends | Device receives |
|---|---|---|
| MQTT 3.1.1, QoS 0 | 141 bytes | nothing |
| MQTT 3.1.1, QoS 1 | 143 bytes | 4 bytes (PUBACK) |
| HTTP/1.1 POST, minimal headers | 275 bytes | about 390 bytes |
| HTTP/1.1 POST, as curl sends it | 312 bytes | about 390 bytes |
MQTT: 1 byte of packet type, 2 of remaining length, 2 of topic length, the 70-byte topic and the 66-byte payload make 141. QoS 1 adds a 2-byte packet identifier and earns a 4-byte PUBACK.
HTTP: the request we captured from curl 8.7.1 carries 246 bytes of headers, 77 of them the Authorization: Bearer line with a 53-character device secret. Without curl's User-Agent and Accept, it's 275 bytes. The response is bigger: our API edge answered a small GET with 346 bytes of headers, so a 202 Accepted with its 37-byte JSON body is about 390.
Every packet then gets the same wrapping: 22 bytes for a TLS 1.3 record (5-byte header, 1-byte content type, 16-byte AES-GCM tag) and at least 40 bytes of TCP/IPv4 headers. On the wire, an MQTT QoS 1 reading is about 205 bytes up and 66 down; an HTTP one about 374 up and 451 down, three times as much.
The TLS handshake is the real cost
We measured handshakes with OpenSSL 3.6 on 19 October 2026, against our API on port 443 and our MQTT broker on 8883, which serve the same certificate:
| Handshake | Server to device | Device to server |
|---|---|---|
| Full, TLS 1.2 | 3,807–4,004 bytes | 305–306 bytes |
| Full, TLS 1.3, X25519 key exchange | 3,914 bytes | 318–319 bytes |
| Full, TLS 1.3, OpenSSL's default post-quantum hybrid | 4,979–4,981 bytes | 1,548–1,553 bytes |
| Resumed session, TLS 1.2 | 141 bytes | 295–504 bytes |
| Resumed session, TLS 1.3 | 267 bytes | 413–622 bytes |
Of the server's share, 3,502 bytes is the four-certificate chain. A full classical handshake is about 4.2 KB, nearly 30 MQTT readings' worth, before the first reading.
It costs time too: a round trip for TCP, one for TLS 1.3 (two for TLS 1.2), one for the request. From the laptop this guide was written on (233 ms average ping to our servers), five fresh HTTPS requests took 0.49–0.79 s each, 0.32–0.54 s of it on TCP and TLS setup. On an open MQTT connection, a QoS 1 publish waits one round trip for its PUBACK, and QoS 0 waits for nothing.
So the real question is how often the device opens a new connection. Reusing one closes most of the gap; reconnecting per reading throws MQTT's advantage away. HTTP reuse has a limit, though: our API closed an idle kept-alive connection after 75 seconds when we measured it, so a device posting less often pays a new (or resumed) handshake every time.
Battery life and cellular data
For an always-on device on mains power, MQTT wins clearly. Here is a month at one reading a minute (43,200 readings) on IPv4, counting TLS and TCP/IP headers but not bare TCP acknowledgements:
| Connection | Per reading | Per 30 days |
|---|---|---|
| MQTT QoS 1, held open | about 271 bytes | about 11.7 MB |
| HTTPS, kept alive between requests | about 825 bytes | about 35.6 MB |
| HTTPS, a new connection every time | about 5.5 KB | about 240 MB |
The last row adds the 4.2 KB handshake and about 500 bytes of packet headers for setup and teardown. MQTT's idle cost is the keepalive ping, sent only after a keepalive period with no other traffic: a 2-byte PINGREQ and a 2-byte PINGRESP, about 64 bytes each way once wrapped, or 5.5 MB a month at a 60-second keepalive on a silent connection.
For a battery device that wakes, sends and sleeps, the usual "MQTT saves power" answer needs a caveat. Each wake pays a full TCP and TLS handshake whichever protocol you pick, and that dwarfs the difference. One MQTT reading is CONNECT with a client ID, credentials and a Last Will (about 200 bytes on Synacl's topics), CONNACK (4), PUBLISH (143), PUBACK (4) and DISCONNECT (2): about 350 bytes, against about 700 over HTTP. Round trips are close too, since MQTT lets a client publish straight after CONNECT without waiting for CONNACK. Battery life follows radio-on time: send several readings per wake, resume TLS sessions if you can, and use the protocol your firmware handles best. MQTT's power advantage comes from holding a connection open, which a sleeping device can't do.
Commands: the part HTTP is bad at
Commands come down: switch a relay, write a setpoint. Over MQTT, the device subscribes to its command topic once, and each command arrives over the open connection within a round trip. With a persistent session (clean session off) and QoS 1, the broker also holds commands published during a brief disconnect and delivers them on reconnect. That cuts both ways: "open the valve" an hour late is a different command. MQTT 5 lets a publisher set a message expiry, but Synacl's commands are MQTT 3.1.1 with no expiry or timestamp today, so for an actuator connect with clean session on, or have the firmware ignore commands that arrive with a reconnect.
Over HTTP the server can't reach the device, which has to ask by polling, long-polling or reading commands from its next POST's response, so a command can wait a whole polling interval. The big platforms agree: AWS IoT Core lists its HTTPS interface as publish-only, and Azure IoT Hub asks HTTPS devices to poll for cloud-to-device messages no more than once every 25 minutes (both checked October 2026).
Either way, a command isn't done until the device acknowledges it, echoing the command's id with the value it applied or an error. Synacl's actuator command contract works like this: the platform publishes {"correlationId": "…", "value": 1} to tenants/{tenantId}/devices/{deviceId}/cmd at QoS 1, the device answers on …/cmd/ack with {"correlationId": "…", "status": "ok", "value": 1, "ts": …}, and the app shows the state as confirmed once the ack arrives. Gateway-wired actuators follow the same contract through the gateway firmware.
Knowing when a device is offline
Over MQTT, the broker knows whether the connection is open, and closes it after one and a half keepalive periods of silence. When a connection ends without a clean DISCONNECT (a power cut, a dead network, a keepalive timeout), the broker publishes the device's Last Will, a message registered at connect time. Make it a retained "offline" status, publish a retained "online" one at connect, and any later subscriber sees the current state.
Over HTTP, the server only knows when it last heard from the device, so "offline" means "silent for longer than expected", and a dead device looks like a slow one until that window runs out.
On Synacl (how online status works):
- The ESP32 gateway connects with a 60-second keepalive and a Last Will, so a gateway that loses power is reported offline about 90 seconds later. It also reports in every 60 seconds, and three missed reports show it offline even if the will never arrives.
- A direct MQTT device publishes a retained
{"ts": …, "reachable": true}on itsstatustopic at connect, with{"ts": …, "reachable": false, "reason": "lwt"}as its will, which raises its offline event when the broker publishes it. Without a will, it goes offline about 90 seconds after its last report by default. - An HTTPS push device gets the same 90-second default, and the window never goes past one hour.
IoT device uptime monitoring goes deeper into offline windows and alerting on silence.
Firewalls, WebSockets and credentials
Port 443 is open almost everywhere; 8883 is often blocked on guest Wi-Fi and locked-down corporate networks, as Azure's protocol guide also warns. MQTT over WebSockets gets around it: the same packets inside a WebSocket on port 443, with the client offering the mqtt subprotocol the spec requires. Synacl's broker takes mqtts://mqtt.synacl.com:8883 and wss://mqtt.synacl.com/mqtt. When we checked, plain port 1883 timed out from the internet, while 8883 and the WebSocket endpoint both answered.
Give every device its own credential, so a leaked or retired one can be cut off alone. A direct MQTT device on Synacl gets its own username and password, shown once, and the broker allows it only its own topics. Use a unique client ID (the device ID works), or two connections keep disconnecting each other. An HTTPS push device gets its own secret, sent as a bearer token or used to sign the raw body with HMAC-SHA256 in an X-Synacl-Signature header, so the secret never crosses the wire.
When HTTP is the right call
- Rare reports. A device sending a few readings a day gains nothing from a persistent connection. On Synacl it will show offline between reports, since the longest window is an hour.
- The sender is software: a cron job, serverless function or server with no process to hold a connection open.
- Service-to-service traffic, where signed webhooks are the norm; on Synacl plans with webhook actions, rules and integrations can call one.
- The device already speaks HTTP. An inverter or meter with a JSON API you can't change can be polled over HTTP if the endpoint is reachable from the internet.
Decision table
| Your situation | Use |
|---|---|
| Mains-powered, readings every few seconds or minutes | MQTT, connection held open |
| Commands must arrive within seconds | MQTT |
| You must know within minutes when it dies | MQTT with a Last Will |
| Metered SIM, frequent readings | MQTT; never reconnect per reading |
| Battery device waking hourly or less | Either; batch readings, resume TLS |
| Script, cron job or serverless function | HTTPS POST |
| Network allows only port 443 | MQTT over WebSockets, or HTTPS |
| Device with a fixed HTTP API | HTTP polling |
How Synacl handles both
Devices behind a Synacl ESP32 gateway (RS485 meters, Modbus equipment, sensors) need no choice: the gateway holds one MQTT connection over TLS for all of them, and you can flash one from the browser. Direct devices can publish over MQTT, push over HTTPS to https://api.synacl.com/ingest/webhook/<deviceId> on paid plans that include it (see the plans page), or be polled over HTTP.
Every route ends in one pipeline: the broker hands each MQTT reading to one of several ingest workers through a shared subscription, and HTTPS pushes reach the same handler, so dashboards, history, rules and integrations don't care how a reading arrived. Rate limits apply alike: a minimum interval per device, one per-second budget per account shared by MQTT and HTTPS, and readings over the limit dropped, not queued. If you're still choosing a platform, our ThingsBoard alternatives guide covers the field. Nearly all take both transports, so ask how each handles commands and offline detection.
To see the MQTT path end to end, ESP32 Modbus RTU to a cloud dashboard takes an RS485 sensor from wiring to a live chart.
Try it on your own hardware
Synacl is free for five devices — no card, no sales call. Flash a gateway from the browser, add your first device, and see live data in a few minutes.