MQTT vs HTTP for IoT: Which Protocol Wins in Production?
Picking a communication protocol for an IoT system feels like it should be straightforward. In practice, the wrong choice costs you battery life, data reliability, and real money on cloud bills. At CodeCaracal we've shipped systems using both — and sometimes both at once. Here's the honest comparison.
Why the Protocol Choice Matters More Than You Think
A microcontroller waking up, sending a reading, and going back to deep sleep is a completely different workload from a streaming sensor publishing 10 readings per second. No single protocol fits both. The decision cascades into hardware selection, cloud architecture, and mobile app design.
Connection Overhead: Where HTTP Hurts the Most
Every HTTP request requires a full TCP handshake (or TLS handshake for HTTPS, which adds two more round trips). On a cellular NB-IoT link with 500 ms RTT, that's over 1.5 seconds of overhead before your 50-byte payload even leaves the device.
MQTT is a persistent connection protocol. The broker handshake happens once at startup. After that, publishing a telemetry message costs as little as 2 bytes of fixed header plus your payload. No headers, no method lines, no status codes.
HTTP POST (TLS, minimal headers) ≈ 800–2000 bytes per request
MQTT PUBLISH (QoS 0) ≈ 4–10 bytes overhead per message
For a device sending one reading per minute, HTTP is perfectly fine. For a device sending 10 readings per second, MQTT saves ~95% of bandwidth.
QoS: MQTT's Built-In Reliability Ladder
HTTP has no native retry semantics — your application code must handle failures. MQTT bakes three Quality-of-Service levels into the protocol:
| Level | Name | Guarantee | |-------|------|-----------| | QoS 0 | Fire-and-forget | Message may be lost | | QoS 1 | At-least-once | Delivered ≥1 times, may duplicate | | QoS 2 | Exactly-once | Delivered exactly once |
For sensor telemetry where losing an occasional reading is acceptable, QoS 0 is ideal — zero acknowledgement overhead. For actuator commands (open valve, trigger alarm), QoS 1 ensures the command lands. For billing or metering data, QoS 2 prevents double-counting at the cost of a four-message handshake.
See the deep-dive in [MQTT QoS 0, 1, and 2 Explained](/blog/mqtt-qos-levels-explained) for full protocol diagrams.
The Pub/Sub Model vs Request/Response
HTTP is fundamentally request/response. Every reading requires the device to initiate a connection. This breaks down immediately when you need to send a command *to* a device — you need long-polling, server-sent events, or WebSockets on top of HTTP to achieve push.
MQTT's pub/sub model solves this naturally. The device subscribes to devices/{deviceId}/commands at startup. The cloud publishes to that topic whenever it needs to send a command. The device receives it within milliseconds, with no polling loop required.
// Node.js: send a command to a specific device
const mqtt = require('mqtt')
const client = mqtt.connect('mqtts://your-broker:8883', {
cert: fs.readFileSync('client.crt'),
key: fs.readFileSync('client.key'),
ca: fs.readFileSync('ca.crt'),
})function sendCommand(deviceId, command) {
const topic = devices/${deviceId}/commands
const payload = JSON.stringify({
action: command,
timestamp: Date.now(),
correlationId: crypto.randomUUID(),
})
client.publish(topic, payload, { qos: 1 }, (err) => {
if (err) console.error('Command delivery failed:', err)
})
}
sendCommand('sensor-42', 'reset_calibration')
With HTTP, achieving the same bi-directional communication requires a webhook receiver on the device side — which means the device needs a public IP or a reverse tunnel. That's a significant complexity and security burden.
Battery Implications
Connection establishment is the single largest energy consumer in most radio stacks. WiFi association + DHCP + TLS handshake on an ESP32 typically consumes 300–500 mA for 1–3 seconds. Deep sleep draws 10–150 µA.
For battery-powered devices that wake every 5 minutes, HTTP over TLS is workable — the radio is on for ~2 seconds per cycle. For devices needing sub-minute intervals, a persistent MQTT connection costs a continuous ~20 mA in idle listen mode, which is often cheaper than repeated reconnection overhead.
Rule of thumb:
HTTP Where It Belongs: Device Provisioning and Bulk Uploads
HTTP isn't wrong — it's just wrong for telemetry. We use it extensively for:
// Node.js: Express endpoint to accept batch sensor upload
app.post('/api/v1/readings/batch', express.json({ limit: '1mb' }), async (req, res) => {
const { deviceId, readings } = req.body
// readings: [{ ts, temperature, humidity }, ...] if (!readings?.length) return res.status(400).json({ error: 'Empty batch' })
const points = readings.map(r => ({
measurement: 'environment',
tags: { deviceId },
fields: { temperature: r.temperature, humidity: r.humidity },
timestamp: new Date(r.ts),
}))
await influxWriteApi.writePoints(points)
await influxWriteApi.flush()
res.json({ accepted: points.length })
})
The Hybrid Architecture
Production systems at CodeCaracal almost always use both protocols. The decision tree:
Is the device battery-powered AND sends data > every 5 min?
├─ YES → persistent MQTT with QoS 0 telemetry
└─ NO → wake-sleep, evaluate interval:
< 2 min → persistent MQTT
> 5 min → HTTP POST or MQTT reconnectDoes the system need cloud-to-device commands?
└─ YES → MQTT required (subscribe to command topic)
Is this firmware OTA or bulk historical upload?
└─ YES → HTTP
Is this device registration or provisioning?
└─ YES → HTTP REST
Real Benchmarks: What We've Seen in Production
Across systems we've deployed for industrial and agricultural clients:
| Metric | MQTT (QoS 0) | HTTPS POST | |--------|-------------|------------| | Message overhead | 4–10 bytes | 800–2000 bytes | | Latency (LTE, connected) | 8–20 ms | 300–800 ms | | Battery (ESP32, 30s interval) | ~12 mA avg | ~18 mA avg | | Cloud egress cost | Very low | Moderate | | Broker/infra complexity | Moderate | Low |
The 8–20 ms MQTT latency assumes a maintained connection. MQTT with a fresh reconnect approaches HTTP latency, so connection pooling matters.
AWS IoT Core: The Managed Middle Ground
If you're deploying on AWS, IoT Core speaks MQTT natively and also exposes an HTTPS endpoint for devices that can't maintain persistent connections. This hybrid endpoint is excellent for low-cost cellular modules that only support HTTP. The payload format is identical — you get the same Rules Engine routing regardless of which transport the device uses.
For a full walkthrough of connecting ESP32 to AWS IoT Core over MQTT, see the [ESP32 MQTT AWS IoT Core Production Guide](/blog/esp32-mqtt-aws-iot-core-production-guide).
Making the Final Call
Use MQTT when:
Use HTTP when:
Use both when (the honest answer for most production systems): your architecture has multiple device classes with different requirements.
Need help with IoT protocol architecture? [Contact Code Caracal](/contact) — we've shipped these systems for clients across 15+ countries.