Une panne de broker MQTT n'est pas une question de si mais de quand : les réseaux se partitionnent, les certificats sont renouvelés, les brokers sont mis à jour et les régions cloud ont leurs mauvais jours. Ce guide vous montre comment concevoir les clients des appareils et du backend pour qu'une panne de broker, courte ou longue, ne fasse perdre aucune donnée importante et ne provoque aucune action en double. Ces patterns s'appliquent à n'importe quel broker, ConnectMQ compris, et s'accompagnent d'un code Python fonctionnel.
Ce qui se passe réellement pendant une panne de broker MQTT
Du point de vue du client, une panne prend l'une de trois formes : la connexion TCP tombe, la connexion se fige silencieusement jusqu'à l'expiration du keepalive, ou les nouvelles tentatives de connexion sont refusées. La cause peut être le broker lui-même, le réseau entre vous et le broker, le DNS, TLS, voire vos propres identifiants. Le client ne peut pas toujours distinguer ces cas : il doit donc les traiter tous de la même manière, détecter vite, continuer à travailler localement et se reconnecter poliment.
Quatre éléments sont en jeu :
- Les données sortantes publiées pendant la déconnexion sont perdues si vous ne les mettez pas en mémoire tampon.
- Les commandes entrantes envoyées pendant que vous étiez hors ligne peuvent être livrées ou non, selon les paramètres de session.
- Les informations de présence deviennent fausses si d'autres systèmes croient l'appareil toujours en ligne.
- Le broker lui-même peut être submergé lorsque des milliers d'appareils se reconnectent au même instant.
Une bonne conception client traite chacun de ces points. Si ces notions vous sont peu familières, lisez ce que fait un broker MQTT des sessions et des messages en file.
Pattern 1 : se reconnecter avec un backoff exponentiel et du jitter
L'approche naïve consiste à réessayer chaque seconde indéfiniment. Multipliez cela par dix mille appareils : dès que le broker revient, il reçoit un mur de connexions simultanées qui peut le faire retomber aussitôt. C'est le problème du « thundering herd ».
La solution est le backoff exponentiel avec jitter aléatoire : attendre 1 s, puis 2 s, 4 s, 8 s et ainsi de suite jusqu'à un plafond, en rendant chaque délai aléatoire pour étaler les appareils. Paho 2.x le prend en charge nativement via reconnect_delay_set, mais n'ajoute pas de jitter ; beaucoup d'équipes gèrent donc la boucle elles-mêmes pour un contrôle total.
Faites aussi attention à vos identifiants. Les brokers qui bloquent les adresses IP après des échecs de connexion répétés, comme le fait ConnectMQ, considéreront un appareil au mot de passe erroné qui réessaie chaque seconde comme une attaque. Temporisez bien plus fortement sur les erreurs d'authentification que sur les erreurs réseau, et remontez-les à un opérateur.
Pattern 2 : mettre en mémoire tampon les données sortantes
Les bibliothèques clientes MQTT conservent les messages en cours d'envoi en mémoire, et ceux-ci disparaissent si l'appareil redémarre pendant la panne. Pour la télémétrie que vous ne pouvez pas vous permettre de perdre, écrivez d'abord chaque mesure dans un petit stockage local et ne la supprimez qu'après l'accusé de réception du broker (PUBACK). SQLite convient bien aux passerelles et aux appareils Linux ; sur les microcontrôleurs, un buffer circulaire en flash remplit le même rôle.
Décidez à l'avance de ce qui se passe quand la mémoire tampon est pleine. Pour la plupart des données de télémétrie, abandonner les mesures les plus anciennes est le bon choix. Pour les événements de facturation ou de sécurité, vous préférerez peut-être cesser d'échantillonner les données non critiques.
Pattern 3 : annoncer la présence avec Last Will et un statut retained
Définissez un Last Will and Testament (LWT) à la connexion : un message offline retained que le broker publie à votre place si vous disparaissez sans déconnexion propre. À chaque connexion réussie, publiez un online retained sur le même topic. Les autres systèmes voient alors une présence exacte sans avoir à interroger l'appareil.
Soyez précis sur la sémantique du LWT. Le will est publié lorsque le broker détecte que la session s'est terminée anormalement, généralement après 1,5 fois l'intervalle de keepalive. Si le broker lui-même est en panne, rien n'est publié avant son retour, et après un redémarrage du broker, l'état retained peut devoir être rétabli par les clients. MQTT 5 ajoute un will delay interval, qui permet de supprimer les messages offline intermittents lors d'une brève reconnexion.
Pattern 4 : rendre les commandes idempotentes
En QoS 1, un message est livré au moins une fois. Lors d'une panne suivie d'une reconnexion, les doublons deviennent probables. Une commande « basculer le relais » livrée deux fois remet le relais à l'arrêt ; « distribuer 10 ml » livrée deux fois distribue 20 ml. Le remède consiste à rendre chaque commande idempotente :
- Envoyez des états absolus (
{"relay": "on"}) plutôt que des actions relatives (toggle). - Incluez un
command_idunique dans chaque commande et faites mémoriser à l'appareil les IDs récemment exécutés. - Incluez une expiration : un message expiry interval MQTT 5, ou un horodatage
valid_until, pour qu'une commande mise en file pendant une longue panne ne soit pas exécutée des heures plus tard, quand elle n'est plus sûre.
Un client Python résilient
Le client suivant combine les quatre patterns. Il stocke la télémétrie dans SQLite, se reconnecte avec un backoff exponentiel aléatoire, maintient un topic de statut retained avec un LWT et déduplique les commandes. Il utilise le serveur MQTT public gratuit pour que vous puissiez l'exécuter immédiatement ; passez au port 8883, à TLS et aux identifiants de votre portail pour la production.
import json
import random
import sqlite3
import time
import uuid
import paho.mqtt.client as mqtt
HOST, PORT = "broker.connectmq.com", 1883
DEVICE = f"dev-{uuid.uuid4().hex[:6]}"
ROOT = f"connectmq/demo/{DEVICE}"
STATUS, TELEMETRY, COMMANDS = f"{ROOT}/status", f"{ROOT}/telemetry", f"{ROOT}/cmd"
db = sqlite3.connect("outbox.db", check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS outbox (id INTEGER PRIMARY KEY, payload TEXT)")
db.commit()
pending = {} # mid -> id de ligne de l'outbox
seen_commands = [] # IDs de commandes récentes pour la déduplication
connected = False
def on_connect(client, userdata, flags, reason_code, properties):
global connected
if reason_code.is_failure:
print("Connect refused:", reason_code)
return
connected = True
print("Connected")
client.publish(STATUS, "online", qos=1, retain=True)
client.subscribe(COMMANDS, qos=1)
def on_disconnect(client, userdata, flags, reason_code, properties):
global connected
connected = False
pending.clear() # les lignes non acquittées restent dans l'outbox et sont renvoyées
print("Disconnected:", reason_code)
def on_publish(client, userdata, mid, reason_code, properties):
row_id = pending.pop(mid, None)
if row_id is not None:
db.execute("DELETE FROM outbox WHERE id = ?", (row_id,))
db.commit()
def on_message(client, userdata, msg):
try:
cmd = json.loads(msg.payload)
except ValueError:
print("Ignoring malformed command")
return
if cmd.get("command_id") in seen_commands:
print("Duplicate command ignored:", cmd["command_id"])
return
if cmd.get("valid_until", float("inf")) < time.time():
print("Expired command ignored:", cmd.get("command_id"))
return
seen_commands.append(cmd.get("command_id"))
del seen_commands[:-100]
print("Executing:", cmd)
def flush_outbox(client):
rows = db.execute("SELECT id, payload FROM outbox ORDER BY id LIMIT 20").fetchall()
for row_id, payload in rows:
if row_id in pending.values():
continue
info = client.publish(TELEMETRY, payload, qos=1)
pending[info.mid] = row_id
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=DEVICE)
client.will_set(STATUS, "offline", qos=1, retain=True)
client.on_connect, client.on_disconnect = on_connect, on_disconnect
client.on_publish, client.on_message = on_publish, on_message
# Backoff exponentiel entre les reconnexions ; un minimum aléatoire étale les appareils
client.reconnect_delay_set(min_delay=random.randint(1, 5), max_delay=120)
client.connect_async(HOST, PORT, keepalive=30) # retentée par la boucle en cas d'échec
client.loop_start()
while True:
reading = {"temperature": round(random.uniform(20, 25), 2), "ts": int(time.time())}
db.execute("INSERT INTO outbox (payload) VALUES (?)", (json.dumps(reading),))
db.commit()
if connected:
flush_outbox(client)
time.sleep(5)
Testez-le en coupant votre réseau pendant une minute. Les mesures s'accumulent dans outbox.db, la boucle réseau continue de réessayer avec des délais croissants, et dès le retour du réseau, l'arriéré se vide dans l'ordre, à raison de vingt lignes maximum par cycle. Paho double le délai entre les tentatives jusqu'à max_delay mais n'ajoute aucun jitter de lui-même : c'est pourquoi chaque appareil choisit un délai de départ aléatoire. Sur une grande flotte, vous pouvez aller plus loin et ajouter un jitter aléatoire à chaque tentative avec votre propre boucle de reconnexion.
L'état de session et ce qu'il ne faut pas présumer
Les sessions persistantes (clean session à false en MQTT 3.1.1, ou un session expiry interval en MQTT 5) permettent au broker de mettre en file les messages QoS 1 et 2 destinés à un client déconnecté. Elles aident lors des pannes côté client. Lors des pannes côté broker, elles n'aident que si le broker conserve les sessions à travers les redémarrages, ce que tous les brokers ne font pas. ConnectMQ ne garantit pas cette persistance à travers les redémarrages, et sur le serveur MQTT public gratuit, les sessions expirent au bout de 30 minutes maximum.
Concevez donc de manière défensive :
- Réabonnez-vous à chaque fois dans
on_connect, plutôt que de compter sur le broker pour se souvenir des abonnements. - Republiez le statut et la configuration retained après la reconnexion.
- Considérez la mémoire tampon de l'appareil, et non les files du broker, comme la source de vérité pour les données que vous ne pouvez pas perdre.
- Laissez le backend réconcilier l'état après une interruption, par exemple en demandant aux appareils de republier leur configuration actuelle.
Checklist de préparation aux pannes
| Domaine | Question | Bonne réponse |
|---|---|---|
| Reconnexion | Chaque client utilise-t-il un backoff avec jitter ? | Oui, plafonné à une ou deux minutes |
| Erreurs d'authentification | Les identifiants erronés sont-ils retentés lentement et signalés ? | Oui, séparément des erreurs réseau |
| Mémoire tampon | Où se trouvent les données non envoyées pendant une panne ? | Stockage local persistant avec une taille limite |
| Présence | Comment les autres apprennent-ils qu'un appareil a disparu ? | LWT plus statut retained |
| Commandes | Que se passe-t-il si une commande arrive deux fois ? | Rien ; les commandes sont idempotentes |
| Commandes périmées | Une ancienne commande peut-elle s'exécuter après une longue panne ? | Non ; l'expiration est appliquée |
| Abonnements | Les abonnements sont-ils restaurés après la reconnexion ? | Oui, dans on_connect |
| Tests | Avez-vous répété une panne ? | Oui, en coupant le réseau lors d'un essai |
Pour les mesures côté broker qui complètent ces patterns côté client, consultez nos guides sur la haute disponibilité d'un broker MQTT et la supervision d'un broker MQTT, et parcourez la checklist de mise en production MQTT avant le lancement.
Faites tourner des clients résilients sur ConnectMQ
Un broker MQTT managé élimine les pannes auto-infligées les plus courantes, comme les certificats expirés et les serveurs non patchés, tandis que vos clients gèrent le reste. Créez un broker gratuit sur app.connectmq.com avec le plan Serverless, sans carte bancaire, testez ces patterns dessus, et utilisez l'état de connexion $SYS et le journal des alertes du portail pour suivre les reconnexions. Découvrez ConnectMQ ou comparez les options dans notre guide du broker MQTT cloud.
Questions fréquentes
À quelle vitesse un client IoT doit-il se reconnecter après une panne de broker MQTT ?
Réessayez rapidement au début, puis espacez de manière exponentielle jusqu'à un plafond d'une à deux minutes, avec un jitter aléatoire sur chaque délai. Vous récupérez ainsi vite après une micro-coupure sans submerger le broker après une longue panne.
La QoS 1 protège-t-elle les données pendant une panne de broker ?
Uniquement pour les messages déjà confiés à la bibliothèque cliente, et uniquement tant que l'appareil reste alimenté. Pour les données que vous ne pouvez pas perdre, stockez-les dans un stockage local persistant jusqu'à l'arrivée d'un PUBACK.
Quand le message Last Will est-il envoyé ?
Lorsque le broker détecte une déconnexion anormale, généralement après environ 1,5 fois l'intervalle de keepalive. Il n'est pas envoyé lors d'une déconnexion propre, et rien n'est envoyé tant que le broker lui-même est en panne.
Les commandes doivent-elles utiliser la QoS 2 pour éviter les doublons ?
La QoS 2 évite les doublons entre le client et le broker, mais des doublons de bout en bout peuvent encore survenir après des reconnexions ou des nouvelles tentatives applicatives. Des commandes idempotentes avec des IDs uniques constituent la solution la plus robuste.



