Broker-Tests

MQTT-Reconnect-Logik und Backoff testen

MQTT-Reconnect-Logik testen: Broker-Neustarts und stille Netzwerkabbrüche simulieren, exponentiellen Backoff, Resubscribe und Session-Wiederaufnahme prüfen.

Aktualisiert · 9 Min. Lesezeit

MQTT-Reconnect-Logik und Backoff testen

Reconnect-Code ist der Teil eines MQTT-Clients, der in der Entwicklung am seltensten und in Produktion am häufigsten läuft. Um MQTT-Reconnect-Logik zu testen, unterbrichst du die Verbindung absichtlich so, wie es die echte Welt tut — Broker-Neustart, abgelehnte Verbindungen, stiller Netzwerkabbruch — und prüfst vier Dinge: Der Client bemerkt es, wartet mit wachsenden Pausen, abonniert erneut, und keine QoS-1-Nachricht geht verloren, während er weg war.

Dieser Guide liefert dir einen protokollierenden Test-Client in Python, drei Fehlerszenarien, die du in Minuten nachstellst, und eine Checkliste mit Prüfpunkten.

Wie "korrektes" Reconnect-Verhalten aussieht

VerhaltenWas du prüfst
ErkennungAbgelehnte/zurückgesetzte Verbindungen sofort; stille Abbrüche innerhalb von ~1–2 Keep-Alive-Intervallen
BackoffPausen wachsen (z. B. 1, 2, 4, 8 … s) bis zu einer Obergrenze und werden nach erfolgreicher Verbindung zurückgesetzt
JitterGeräte versuchen es nicht im Gleichschritt — sonst entsteht ein Reconnect-Sturm
ResubscribeSubscriptions werden wiederhergestellt, egal ob der Broker die Session behalten hat
Session-WiederaufnahmeMit persistenter Session kommen gepufferte QoS-1/2-Nachrichten nach dem Reconnect an
Richtig aufgebenAuthentifizierungsfehler werden deutlich geloggt, statt den Broker endlos zu bombardieren

Die Design-Seite — wie du Pausen und Session-Einstellungen wählst — behandelt Best Practices für MQTT-Reconnects. Hier geht es darum, zu beweisen, dass dein Client es tut.

Wie gängige Bibliotheken neu verbinden

  • paho-mqtt (Python 2.x) verbindet sich innerhalb von loop_forever() und loop_start() automatisch neu. reconnect_delay_set(min_delay, max_delay) steuert den exponentiellen Backoff; Jitter gibt es nicht.
  • MQTT.js 5 verbindet sich automatisch alle reconnectPeriod Millisekunden neu (Standard 1000) — ein festes Intervall, nicht exponentiell. Bereits abonnierte Topics werden standardmäßig neu abonniert (resubscribe: true).
  • Eclipse Paho Java verbindet sich neu, wenn setAutomaticReconnect(true) gesetzt ist, und verdoppelt die Pause bis zu einem konfigurierbaren Maximum.
  • MQTTnet überlässt den Reconnect deinem Code: Du behandelst das Disconnected-Event und rufst connect mit eigener Pause erneut auf.

Egal welche Bibliothek: Die Standardwerte sind selten das, was du in Produktion willst — genau deshalb verdienen sie einen Test.

Ein Test-Client, der jeden Übergang protokolliert

Dieser paho-mqtt-2.x-Client nutzt MQTT 5 mit persistenter Session, abonniert bei jeder Verbindung neu und gibt für jedes Ereignis einen monotonen Zeitstempel aus, sodass du den Backoff direkt in der Ausgabe ablesen kannst:

reconnect_probe.py
import os, time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

HOST = os.getenv("MQTT_HOST", "localhost")
PORT = int(os.getenv("MQTT_PORT", "1883"))
TOPIC = "testmqtt/reconnect/probe/#"
T0 = time.monotonic()

def log(event, **info):
    print(f"{time.monotonic() - T0:8.2f}s  {event:<15} {info}", flush=True)

def on_connect(client, userdata, flags, reason_code, properties):
    log("connected", rc=str(reason_code), session_present=flags.session_present)
    if not reason_code.is_failure:
        client.subscribe(TOPIC, qos=1)   # immer neu abonnieren

def on_disconnect(client, userdata, flags, reason_code, properties):
    log("disconnected", rc=str(reason_code))

def on_connect_fail(client, userdata):
    log("connect_failed")

def on_message(client, userdata, msg):
    log("message", topic=msg.topic, payload=msg.payload.decode(), dup=msg.dup)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
                     client_id="reconnect-probe-1", protocol=mqtt.MQTTv5)
if os.getenv("MQTT_USER"):
    client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect, client.on_disconnect = on_connect, on_disconnect
client.on_connect_fail, client.on_message = on_connect_fail, on_message
client.reconnect_delay_set(min_delay=1, max_delay=32)

props = Properties(PacketTypes.CONNECT)
props.SessionExpiryInterval = 3600       # Session 1 Stunde behalten
client.connect_async(HOST, PORT, keepalive=10, clean_start=False, properties=props)
client.loop_forever(retry_first_connection=True)

Halte die Client-ID beim Testen fest — eine persistente Session gehört zu einer Client-ID, und zwei Prozesse mit derselben ID werfen sich gegenseitig immer wieder hinaus. Ein kurzes keepalive=10 lässt stille Abbrüche schnell sichtbar werden; nimm für den finalen Lauf deinen Produktionswert.

Szenario 1: Broker-Neustart und Ausfall

Starte lokal einen Wegwerf-Mosquitto, den du jederzeit stoppen kannst:

Lokaler Broker zum Abschießen
docker run -d --name mqtt-reconnect -p 1883:1883 eclipse-mosquitto:2 \
  mosquitto -c /mosquitto-no-auth.conf
python reconnect_probe.py            # Terminal 1

docker stop mqtt-reconnect && sleep 60 && docker start mqtt-reconnect   # Terminal 2

Erwartete Ausgabe, mit sich verdoppelnden Pausen, bis der Broker zurück ist:

So sieht korrekter Backoff aus
    0.03s  connected       {'rc': 'Success', 'session_present': False}
   12.40s  disconnected    {'rc': 'Unspecified error'}
   13.41s  connect_failed  {}
   15.42s  connect_failed  {}
   19.43s  connect_failed  {}
   27.44s  connect_failed  {}
   43.45s  connect_failed  {}
   75.46s  connected       {'rc': 'Success', 'session_present': False}

Die genauen Reason-Code-Texte variieren je nach Version, aber die Abstände zwischen den Versuchen sollten 1, 2, 4, 8, 16 und 32 Sekunden betragen. Beachte session_present: False nach dem Neustart: Dieser Mosquitto hat keine Persistenz, die Session ist also mit dem Prozess gestorben. Dein Client muss neu abonnieren — kommen nach diesem Test keine Nachrichten mehr an, ist genau das der Bug. Aktiviere persistence true in Mosquitto (siehe Mosquitto-Persistenz) und wiederhole den Test, um die Session einen Neustart überleben zu sehen.

Szenario 2: stiller Netzwerkabbruch

Ein Broker-Neustart schließt Sockets sauber. Ein hängender WLAN-Access-Point oder ein NAT-Timeout nicht — Pakete verschwinden einfach, und der Client erfährt es nur über den Keep-Alive. Auf einer Linux-Maschine simulierst du das mit einer Firewall-Regel:

Allen Traffic zu Port 1883 verwerfen, dann wiederherstellen
sudo iptables -I OUTPUT -p tcp --dport 1883 -j DROP
sleep 45
sudo iptables -D OUTPUT -p tcp --dport 1883 -j DROP

Nutze -j REJECT --reject-with tcp-reset statt -j DROP, um einen sofortigen Reset zu simulieren. Der Unterschied ist wichtig: Mit DROP sollte die Probe etwa 10–20 Sekunden nach dem Setzen der Regel disconnected loggen (ein bis zwei Keep-Alive-Intervalle) und dann per Backoff warten, bis die Regel entfernt ist. Loggt sie nie einen Disconnect, funktioniert der Keep-Alive nicht — siehe MQTT Keep Alive.

Für Latenz, Paketverlust, Bandbreitenlimits oder plattformübergreifende Setups ist ein Proxy flexibler als iptables; Testen mit Toxiproxy zeigt, wie du solche Fehler zwischen Client und Broker einschleust.

Szenario 3: Session-Wiederaufnahme mit gepufferten Nachrichten

Diesen Test lassen die meisten Teams aus — und aus ihm stammen die meisten Incidents. Richte die Probe auf einen Broker, der Sessions behält, zum Beispiel den geteilten Endpunkt broker.connectmq.com, und:

  1. Starte reconnect_probe.py und warte auf connected mit aktiver Subscription.
  2. Blockiere Port 1883 mit der iptables-Regel aus Szenario 2.
  3. Während die Probe offline ist, publishe fünf QoS-1-Nachrichten auf testmqtt/reconnect/probe/q über einen Weg, den die Regel nicht blockiert — zum Beispiel den Online-MQTT-Client, der über sichere WebSockets auf Port 8084 verbindet.
  4. Entferne die Regel und beobachte, wie sich die Probe neu verbindet.

Die Probe sollte session_present: True und danach alle fünf Nachrichten loggen. Verbindet sie sich mit session_present: False, hat der Broker die Session verworfen: Prüfe clean_start, das Session-Expiry-Intervall und ob sich deine Client-ID geändert hat. Kommen Nachrichten mit dup: True oder doppelt an, ist das QoS-1-At-least-once-Verhalten wie vorgesehen — dein Handler muss idempotent sein. Persistente Sessions erklärt, was der Broker wie lange aufbewahrt.

Weitere Fehlerfälle, die einen Test wert sind

  • Zugangsdaten offline geändert. Der Broker beantwortet den Reconnect mit "Not authorized" oder "Bad user name or password". Der Client sollte das deutlich loggen und langsamer werden, statt ewig jede Sekunde neu zu versuchen. Codes schlägst du im Reason-Code-Tool nach.
  • DNS-Fehler. Trenne das Netzwerk komplett; Auflösungsfehler müssen denselben Backoff-Pfad nehmen wie abgelehnte Verbindungen.
  • Doppelte Client-ID. Starte die Probe zweimal. Jede Instanz wirft die andere hinaus, und beide verbinden sich endlos neu — eine klassische Produktionsschleife.
  • Lange Ausfälle. Lass den Broker länger als max_delay aus und prüfe, dass sich die Versuche bei der Obergrenze einpendeln, statt weiter zu wachsen oder aufzuhören.
  • Reconnect während des Publishens. Publishe QoS-1-Nachrichten in einer Schleife, während du das Netzwerk kappst, und zähle, was nach der Erholung bei einem Subscriber ankommt.

Checkliste für Reconnect-Tests

  1. Logge jede Verbindung, jeden Disconnect und jeden fehlgeschlagenen Versuch mit monotonem Zeitstempel.
  2. Starte den Broker neu: Prüfe Erkennung, wachsende Pausen und Erholung.
  3. Verwirf Traffic still: Prüfe die Keep-Alive-Erkennung innerhalb von ein bis zwei Intervallen.
  4. Puffere QoS-1-Nachrichten, während der Client offline ist: Prüfe session_present und die Zustellung nach dem Reconnect.
  5. Prüfe das Resubscribe nach einem Neustart ohne Persistenz.
  6. Teste falsche Zugangsdaten, DNS-Fehler und doppelte Client-IDs.
  7. Automatisiere die Szenarien in CI mit einem Wegwerf-Broker-Container.

Reconnect-Logik ist nur so gut wie das letzte Mal, als du sie funktionieren gesehen hast. Eine Probe mit Zeitstempeln, ein Broker, den du stoppen kannst, und eine Firewall-Regel genügen, um Backoff, Resubscribe und Session-Wiederaufnahme zu beweisen — und die Regression zu finden, bevor ein Broker-Neustart es für dich tut.

Häufig gestellte Fragen

Warum empfängt mein MQTT-Client nach dem Reconnect keine Nachrichten mehr?

Meist wurde die Session nicht wiederaufgenommen: Der Client hat sich mit Clean Session verbunden (oder der Broker hat die Session beim Neustart verloren) und nicht neu abonniert. Abonniere im Connect-Callback jedes Mal neu und prüfe das Session-Present-Flag im CONNACK.

Wie lange braucht ein MQTT-Client, um eine tote Verbindung zu erkennen?

Bricht das Netzwerk still ab, merkt der Client es erst, wenn sein Keep-Alive-Ping unbeantwortet bleibt — typischerweise innerhalb von ein bis zwei Keep-Alive-Intervallen. Eine abgelehnte oder zurückgesetzte Verbindung wird sofort erkannt.

Nutzt paho-mqtt exponentiellen Backoff?

Ja. Mit loop_forever() oder loop_start() verbindet sich paho-mqtt automatisch neu und verdoppelt die Wartezeit von min_delay bis max_delay, gesetzt mit reconnect_delay_set(). Zufälligen Jitter fügt es nicht hinzu, ergänze ihn selbst, wenn viele Geräte denselben Rhythmus haben.