MQTT Security

MQTT Rate Limiting and DoS Protection Explained

How MQTT brokers defend against connection floods, publish storms and oversized payloads, what limits a managed broker applies, and how to write clients that respect them.

MQTT rate limiting diagram with a broker throttling a publish storm while normal IoT devices keep sending messages
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

MQTT rate limiting is how a broker protects itself and its users from clients that send too much, too fast or too large. This article explains the main denial-of-service patterns on MQTT, the limits brokers use against them, how ConnectMQ applies quotas and flood protection, and how to write clients that back off instead of making the problem worse. Most rate-limit incidents are caused by buggy firmware rather than attackers, so both sides matter.

Why MQTT Needs Rate Limiting

An MQTT broker is a shared resource. Every connection costs memory, every message costs CPU and bandwidth, and every subscription adds routing work. Without limits, one misbehaving client can degrade service for every other client on the same broker.

The threat is not only malicious. Common causes of overload include:

  • A firmware bug that publishes in a tight loop instead of once per minute.
  • A reconnect loop with no delay after a network glitch, repeated across a whole fleet.
  • A camera or logger accidentally publishing large binary blobs.
  • A dashboard subscribing to # on a busy broker and fanning out every message.
  • Credential guessing that opens connection after connection.

Rate limiting turns these failures from outages into contained, visible errors.

The Main DoS Patterns on MQTT

Pattern What happens Typical defence
Connection flood Many TCP/TLS connections opened quickly, each costing a handshake and memory Per-IP connection rate limits, max connections per tenant, IP blocking
Reconnect storm Thousands of devices reconnect at the same moment after an outage Client-side exponential backoff with jitter
Publish storm A client sends far more messages per second than expected Per-client message rate limits, disconnect on abuse
Oversized payloads Huge messages consume memory and bandwidth Maximum packet size
Subscription explosion A client creates many subscriptions or broad wildcards Max subscriptions per client, topic depth limits
Retained or session bloat Many retained topics or long-lived sessions with queued messages Retained message limits, session expiry
Slow consumers Subscribers that cannot keep up cause queues to grow Queue limits, QoS choices, message expiry
Credential guessing Repeated failed logins Failed-login throttling and IP blocking

How Brokers Enforce Limits

Connection-Level Limits

The cheapest place to stop abuse is before a client is authenticated. Brokers limit how many connections a single IP may open per second, cap the total connections per account or plan, and block sources that repeatedly fail authentication. A TLS handshake is relatively expensive, so throttling at this stage protects CPU as well as memory.

Message-Level Limits

Once connected, the broker can track messages per second per client and bytes per second. When a client exceeds the budget, the broker has a few options: delay processing, drop messages, refuse them with a reason code or disconnect the client. MQTT 5 makes this far more transparent than MQTT 3.1.1.

Packet Size Limits

Every broker has a maximum packet size. MQTT 5 lets both sides advertise it: the broker sends Maximum Packet Size in CONNACK, and clients should never send more. A message above the limit is a protocol error and typically results in disconnection.

Subscription and Topic Limits

Caps on subscriptions per client and on topic depth stop clients from creating unbounded routing work.

MQTT 5 Features That Help

MQTT 5 added several mechanisms that make limits explicit instead of mysterious. The overview of MQTT 5 features covers them in more detail.

  • Reason codes. A broker can reply with 0x97 Quota exceeded on a PUBACK or a DISCONNECT, or 0x96 Message rate too high, instead of silently dropping data. Clients can log the reason and slow down.
  • Receive Maximum. Both sides announce how many unacknowledged QoS 1 and QoS 2 messages they will accept in flight, which provides basic flow control.
  • Maximum Packet Size. Announced in CONNECT and CONNACK so oversized messages are avoided rather than rejected.
  • Message expiry. Old messages are discarded instead of piling up for offline subscribers.
  • Session expiry. Abandoned sessions are cleaned up automatically.
  • Server Keep Alive. The broker can override a client's keep-alive interval.

With MQTT 3.1.1, a broker that enforces limits can usually only drop messages or close the connection, and the client sees a generic network error.

Limits on ConnectMQ

ConnectMQ applies limits at several layers to protect tenants on its shared clusters:

  • Per-plan quotas. The free Serverless plan allows up to 25 connections, QoS up to 1 and 256 KB messages. The paid Serverless plan raises this to up to 1,000 connections, QoS 2 and 1 MB messages. Dedicated and BYOC deployments run on isolated infrastructure.
  • Quota signalling. When a quota is exceeded, MQTT 5 clients receive reason code 0x97 (quota exceeded), so you can tell a limit apart from a network failure.
  • Flood protection against abusive connection and message patterns.
  • IP blocking after repeated failed logins.
  • Tenant isolation, so one tenant's traffic cannot reach another's topics.

The public test broker, broker.connectmq.com, has stricter per-client limits because it is open to everyone: 64 KB per message, 10 messages per second per client, 10 subscriptions per client, 8 topic levels and session expiry up to 30 minutes. If a test hits these limits, that is a strong hint to move the workload to a private broker. The free public MQTT broker article lists the details.

Writing Clients That Respect Limits

Server-side limits are only half the story. A fleet of well-behaved clients is the best DoS protection you can have.

  1. Back off on reconnect. Use exponential backoff with random jitter so devices do not reconnect in lockstep after an outage.
  2. Batch and aggregate. Send one message with ten readings instead of ten messages, where latency allows.
  3. Publish on change, not on every loop. Add deadbands so tiny fluctuations do not generate traffic.
  4. Respect the advertised packet size. Read Maximum Packet Size from CONNACK properties.
  5. Handle reason codes. Log 0x97 and 0x96 and reduce your publish rate rather than retrying immediately.
  6. Keep subscriptions narrow. Subscribe to the topics you need, not #.
  7. Use unique client IDs. Two devices sharing an ID will kick each other off in an endless loop that looks like a flood.

Here is a Python client using paho-mqtt 2.x that applies backoff, handles quota reason codes and throttles its own publishing:

import json
import random
import time
import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 8883
CLIENT_ID = f"rate-demo-{random.randint(1000, 9999)}"
TOPIC = f"connectmq/demo/{CLIENT_ID}/telemetry"
MAX_RATE = 2.0  # messages per second, well below the public broker limit

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID,
                     protocol=mqtt.MQTTv5)
client.tls_set()
# For a private broker add: client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.reconnect_delay_set(min_delay=1, max_delay=120)  # exponential backoff

slow_down_until = 0.0

def on_connect(c, userdata, flags, reason_code, properties):
    print("connected:", reason_code)
    if properties and hasattr(properties, "MaximumPacketSize"):
        print("broker max packet size:", properties.MaximumPacketSize)

def on_publish(c, userdata, mid, reason_code, properties):
    global slow_down_until
    if reason_code.is_failure:
        print(f"publish {mid} refused: {reason_code}")
        if reason_code.value in (0x96, 0x97):
            slow_down_until = time.time() + 30  # pause for 30 s

def on_disconnect(c, userdata, flags, reason_code, properties):
    print("disconnected:", reason_code)

client.on_connect = on_connect
client.on_publish = on_publish
client.on_disconnect = on_disconnect

client.connect(HOST, PORT, keepalive=60)
client.loop_start()

interval = 1.0 / MAX_RATE
try:
    while True:
        if time.time() < slow_down_until:
            time.sleep(1)
            continue
        payload = json.dumps({"t": round(20 + random.random(), 2), "ts": int(time.time())})
        client.publish(TOPIC, payload, qos=1)
        time.sleep(interval)
except KeyboardInterrupt:
    client.loop_stop()
    client.disconnect()

The same principles apply to firmware. On an ESP32, put a delay with jitter before each reconnect attempt and never call publish() from a loop without a timer; the ESP32 MQTT tutorial shows a non-blocking pattern.

Monitoring for Rate-Limit Problems

Watch for these signals in your client logs and broker dashboards:

  • Spikes in disconnections across many devices at the same moment.
  • Repeated 0x97 or 0x96 reason codes from the same client.
  • Devices that connect, publish and disconnect in a cycle of a few seconds.
  • A message rate that does not match the configured reporting interval.

On ConnectMQ, the portal dashboard and live topic explorer help you spot a chatty client, and $SYS/broker/connection/{clientId}/state lets you watch a particular client's connection state.

When to Move to a Bigger Plan

If legitimate traffic, not a bug, is hitting limits, the answer is capacity rather than tighter clients. Signs include steady growth in connected devices, a need for QoS 2, or payloads above 256 KB. The managed MQTT broker guide explains how serverless and dedicated options differ, and the related article on exposed MQTT brokers shows why self-hosted brokers without limits are a risk.

Get Started with ConnectMQ

Create a broker on the free Serverless plan at app.connectmq.com with no credit card, connect the client above with your generated credentials, and see how quotas and reason codes behave in practice. For broader guidance, read MQTT security best practices.

Frequently Asked Questions

What does MQTT reason code 0x97 mean?

It means "Quota exceeded". The broker is telling the client it has gone over an allowed limit, so the client should slow down rather than retry immediately.

Does MQTT 3.1.1 support rate-limit feedback?

Not directly. MQTT 3.1.1 has no reason codes on PUBACK, so a broker can only drop messages or disconnect. Upgrade clients to MQTT 5 to receive explicit feedback.

Should I rate limit on the device as well?

Yes. Client-side throttling, batching and reconnect backoff prevent most incidents, and they keep you well inside broker limits.

What is a reconnect storm?

It happens when many devices lose connection at the same time and all retry immediately. Exponential backoff with random jitter spreads reconnections over time.

Related articles

How to Rotate MQTT Credentials Without Downtime

A step-by-step dual-credential strategy for rotating MQTT usernames and passwords across a fleet: stage the new credential, switch with fallback, verify and only then revoke the old one.

7 min read

Storing MQTT Credentials Securely on Embedded Devices

A practical comparison of ways to protect MQTT usernames and passwords on ESP32 and Linux devices, from encrypted NVS and flash encryption to secure elements and TPM-backed systemd credentials.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.