MQTT vs HTTP for IoT: Choosing the Right Protocol

· 11 min · comparison

MQTT · HTTP · IoT · Protocols · Network

Every IoT project eventually hits the same decision point: should the device talk to the cloud over MQTT or HTTP? Both are mature, well-supported protocols with excellent library support in every language you are likely to use. Both will work. The question is which one works better for your specific use case — and the answer depends on a set of trade-offs that are not always obvious until you have deployed something at scale and watched it behave under real network conditions.

I have used both extensively: MQTT for the production edge client running across deployed robot units and warehouse sensors, and HTTP for the BMS monitoring dashboard backend and the smart entry system’s Flask API. The protocols feel genuinely different to build with, and those differences have downstream consequences on battery life, bandwidth, broker infrastructure, and how much complexity ends up in your application code. This article works through those trade-offs concretely.

The Fundamental Architectural Difference

Before comparing specific metrics, it helps to understand what each protocol was designed to do, because the design intent shapes every trade-off that follows.

HTTP was designed for the web — a request/response model where a client asks for something and a server responds. Every HTTP interaction is a complete, self-contained transaction. The client connects, makes a request, receives a response, and disconnects. The server holds no memory of previous requests unless you explicitly build that state in through cookies or tokens. This statelessness is one of HTTP’s great strengths for web applications: it makes servers easy to scale horizontally, because any server can handle any request. But it is also the source of nearly all of HTTP’s inefficiencies for IoT.

MQTT was designed for machine-to-machine communication on unreliable, low-bandwidth networks — the original use case was monitoring oil pipelines over satellite links in the 1990s. It is a publish/subscribe protocol built on a persistent TCP connection. A device connects to a broker once and stays connected. Publishing a message does not require a new connection or a new TCP handshake; it is simply a small packet sent over the already-open connection. The broker routes messages to any subscribers that have expressed interest in the relevant topic. The device never directly addresses any downstream consumer.

This architectural difference — request/response versus persistent publish/subscribe — is the root cause of every performance difference between the two protocols. Everything else follows from it.

Bandwidth and Packet Overhead

The most concrete way to see the difference is to measure what actually travels over the wire for a single sensor reading.

An HTTP POST with a JSON payload looks like this on the wire:

POST /api/sensors/temperature HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Length: 52
Connection: keep-alive

{"device_id": "sensor_01", "temperature": 23.5}

The JSON payload itself is 48 bytes. The HTTP headers — Host, Content-Type, Authorization, Content-Length — add roughly 200–400 bytes depending on token length and header verbosity. A typical HTTPS connection also requires a TLS handshake of around 4–6 KB at session establishment. If you are opening a new TCP connection per request (which is what happens if you do not explicitly configure keep-alive or use HTTP/2), add another TCP three-way handshake on top.

An equivalent MQTT PUBLISH packet for the same payload looks like this:

Fixed header:  2 bytes   (packet type + flags + remaining length)
Topic length:  2 bytes
Topic:         ~25 bytes  (e.g. "sensors/warehouse/temp01")
Payload:       48 bytes
Total:         ~77 bytes

The MQTT fixed header is two bytes. The total overhead for a published message is roughly the topic length plus a handful of bytes of protocol framing. Over an established persistent connection, there is no handshake, no TLS renegotiation, no HTTP header block — just the packet.

For a device publishing every five seconds, the bandwidth difference is not dramatic in absolute terms. But for a device publishing ten times per second, or a fleet of 500 devices publishing at moderate rates, the cumulative difference becomes significant. More importantly, each HTTP request requires the device’s radio to wake up and complete a full TCP handshake before it can send a byte of data. On a cellular-connected device, that radio wake-up can cost more energy than the data transfer itself.

Connection Persistence and Latency

HTTP’s request/response model means the server cannot send data to the device unless the device first asks for it. If you want your server to push a command to a device — update a configuration parameter, trigger an action, change a setpoint — you have two options with HTTP: either the device polls continuously (GET /commands every N seconds), or you implement something like long-polling or Server-Sent Events, which are essentially workarounds for the fact that HTTP was not designed for server-initiated communication.

# HTTP polling for commands — works, but wastes bandwidth and adds latency
import requests
import time

def poll_for_commands(device_id: str, interval_seconds: int = 5):
    while True:
        response = requests.get(
            f"https://api.example.com/devices/{device_id}/commands",
            headers={"Authorization": f"Bearer {TOKEN}"},
            timeout=10
        )

        if response.status_code == 200:
            commands = response.json()
            for command in commands:
                handle_command(command)

        # Even if there are no commands, we made a full HTTP request.
        # At 5-second intervals, that is 17,280 requests per day per device.
        time.sleep(interval_seconds)

MQTT handles bidirectional communication natively. The device subscribes to a command topic at connection time, and any message published to that topic by the server is delivered immediately over the existing persistent connection. No polling, no long-polling workarounds, no latency introduced by polling intervals.

import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    # Subscribe to command topic immediately on connection.
    # Commands arrive in real time — no polling required.
    client.subscribe(f"devices/{DEVICE_ID}/commands", qos=1)

def on_message(client, userdata, msg):
    command = json.loads(msg.payload)
    handle_command(command)

client = mqtt.Client(client_id=DEVICE_ID, clean_session=False)
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER_HOST, 1883)
client.loop_forever()

The latency difference is measurable. HTTP polling at five-second intervals means a command can wait up to five seconds before the device even learns it exists. MQTT delivers it in the time it takes a packet to travel from broker to device over the established TCP connection — typically single-digit milliseconds on a local network, sub-second on a cellular connection.

For the telepresence robot, this distinction is not academic. PTZ camera control commands need to arrive within milliseconds, not seconds. HTTP polling is not a viable architecture for real-time control regardless of how convenient it might be to implement.

Power Consumption

On battery-powered devices, the radio is almost always the dominant power consumer. The cost of transmitting data is secondary to the cost of establishing and maintaining the radio connection in the first place.

HTTP’s connection-per-request model is expensive because the device’s radio has to wake from sleep, establish a TCP connection (and a TLS session if using HTTPS), transmit the request, receive the response, and then either hold the connection open or close it. Each of those steps burns power, and most of that power is consumed in the radio wake-up and the handshakes, not the data transfer itself.

MQTT’s persistent connection model seems like it would be worse — the radio is always on — but in practice MQTT supports a keepalive mechanism that allows extremely low-power operation. By setting a short keepalive interval and using MQTT’s built-in PINGREQ/PINGRESP mechanism, a device can maintain logical connection presence while the underlying TCP connection uses the OS’s TCP keepalive with aggressive idle timeouts. More importantly, MQTT is designed to work with sleep cycles: a device can publish data, rely on the broker to queue any incoming messages (via persistent session), and then sleep with its radio off. When it wakes up, it reconnects and immediately receives any queued messages. The broker has been holding the messages; the device did not need to poll.

The trade-off inverts when the device genuinely only needs to send data occasionally and never needs to receive anything. A temperature sensor that wakes up once an hour, posts a reading to an HTTP endpoint, and goes back to sleep does not benefit from MQTT’s connection persistence — the connection would be established and torn down regardless. In that narrow scenario, HTTP is simpler and the power profile is comparable.

Infrastructure Complexity

HTTP requires no specialised server infrastructure beyond a standard web server or cloud function. Every cloud provider offers HTTP endpoints out of the box, and deploying an HTTP API takes minutes with tools like Flask, FastAPI, or Express. The debugging tooling is excellent — curl, Postman, browser dev tools, and every language’s standard HTTP library all speak HTTP natively. If something breaks, the error shows up clearly in server logs with a standard status code.

# HTTP backend for receiving sensor data — minimal infrastructure required
from flask import Flask, request, jsonify
import sqlite3
import time

app = Flask(__name__)

@app.route('/api/readings', methods=['POST'])
def receive_reading():
    data = request.json
    conn = sqlite3.connect('readings.db')
    conn.execute(
        'INSERT INTO readings (timestamp, device_id, value) VALUES (?, ?, ?)',
        (time.time(), data['device_id'], data['value'])
    )
    conn.commit()
    return jsonify({'status': 'ok'}), 201

MQTT requires a broker. Mosquitto is the standard open-source option and runs well on a Raspberry Pi or a small cloud VM, but it is a separate service to deploy, configure, monitor, and keep running. You need to think about authentication (username/password or TLS client certificates), topic access control lists, persistence configuration, and what happens when the broker restarts. Managed MQTT brokers (HiveMQ Cloud, AWS IoT Core, CloudMQTT) remove the operational burden but add cost and vendor dependency.

The broker is also a single point of failure in a way that a horizontally-scaled HTTP API is not. If the broker goes down, all devices lose connectivity simultaneously. This is why the offline queue in the production MQTT client exists — the assumption is that broker downtime is a real possibility, and the client must handle it gracefully rather than losing data. HTTP’s stateless architecture makes it easier to scale and harder to fail completely, since any HTTP server can handle any request and you can put a load balancer in front of multiple instances trivially.

Fan-Out and Many-to-Many Communication

One area where MQTT has a capability that HTTP cannot replicate easily is many-to-many message distribution. When a sensor publishes a reading, every subscriber to that topic receives it — a dashboard, a logging service, an alerting system, and an analytics pipeline can all receive the same message simultaneously without the device knowing any of them exist. Adding a new subscriber does not require any changes to the publisher.

Sensor publishes to: sensors/warehouse/temp01

Subscribers:
  Dashboard service   ← receives it automatically
  Alert service       ← receives it automatically
  Time-series DB      ← receives it automatically
  ML anomaly detector ← receives it automatically (added later, no sensor changes)

Replicating this with HTTP requires either a webhook fan-out system (the server receiving the POST forwards it to multiple downstream services) or a message queue like RabbitMQ or SQS sitting in front of the HTTP endpoint. You are essentially building a pub/sub system on top of HTTP, which means you have added the infrastructure complexity of MQTT while keeping HTTP’s connection overhead.

Where Each Protocol Belongs

After working with both protocols in production deployments, the decision framework I have settled on is cleaner than “MQTT is for IoT, HTTP is for web.” The right question is what communication pattern the device actually needs.

HTTP is the right choice when the device needs to talk to an existing web service or cloud API that is already HTTP-based and not worth wrapping behind a broker. It is also appropriate for infrequent, data-heavy transmissions — sending a firmware update, uploading a captured image, or posting a batch of readings that accumulated over hours. HTTP is also vastly simpler for one-way data ingestion when you have no need for server-to-device communication and you want zero broker infrastructure.

MQTT is the right choice when the device needs to receive commands or configuration updates from the server in real time. It is also appropriate when you have a fleet of devices that all produce data consumed by multiple downstream services — the pub/sub fan-out is genuinely valuable architecture, not just a protocol preference. MQTT is essential when network reliability is poor and you need QoS guarantees, offline buffering, and reconnection resilience. And it is the right choice when bandwidth and power efficiency matter at scale, because the overhead difference that looks small per-device becomes significant across hundreds of devices.

In practice, many production systems use both. The device publishes sensor telemetry over MQTT because it is continuous, bidirectional, and benefits from QoS guarantees. A separate HTTP endpoint handles firmware updates, large file uploads, and integration with third-party services that speak REST. The MQTT broker handles real-time operational communication; the HTTP API handles administrative and integration traffic. This is not a failure to choose — it is using each protocol for what it is actually good at.

Comments