MQTT bị mất kết nối là chuyện hoàn toàn bình thường trong hệ thống IoT thực tế: broker khởi động lại, mạng WiFi chập chờn, ESP32 bị reset. Vấn đề không phải là “làm sao để không bao giờ mất kết nối” mà là làm sao reconnect đúng cách mà không làm chương trình bị treo.
Bài này đi từ nguyên nhân, mã lỗi, pattern sai phổ biến, đến giải pháp thực tế cho dự án production.
Tại Sao MQTT Mất Kết Nối?
Trước khi fix, cần biết lý do. Có 4 nhóm nguyên nhân chính:
| Nhóm | Nguyên Nhân Cụ Thể |
|---|---|
| Mạng | WiFi chập chờn, router reset, IP thay đổi |
| Broker | Broker restart, overload, timeout |
| Cấu hình | Client ID trùng, sai credentials, keep-alive quá ngắn |
| Code | loop() chạy quá lâu, không gọi mqtt.loop() đều |
Lỗi hay gặp nhất trong thực tế: client ID trùng nhau khi deploy nhiều thiết bị cùng code mẫu, và loop() bị chặn bởi delay() hoặc tác vụ nặng làm MQTT keep-alive timeout.
Đọc Mã Lỗi PubSubClient
Khi mqtt.connect() thất bại hoặc mqtt.connected() trả về false, gọi mqtt.state() để biết lý do chính xác:
int state = mqtt.state();
| Mã | Tên Hằng Số | Ý Nghĩa | Cách Fix |
|---|---|---|---|
| -4 | MQTTCONNECTIONTIMEOUT | Broker không phản hồi trong thời gian quy định | Kiểm tra IP/port broker, tường lửa |
| -3 | MQTTCONNECTIONLOST | Kết nối TCP bị đứt giữa chừng | Thường do mạng — cần reconnect |
| -2 | MQTTCONNECTFAILED | Không tạo được kết nối TCP tới broker | Broker chưa chạy, sai địa chỉ |
| -1 | MQTT_DISCONNECTED | Client chủ động ngắt hoặc chưa kết nối | Bình thường khi mới khởi động |
| 0 | MQTT_CONNECTED | Đang kết nối bình thường | ✓ |
| 1 | MQTTCONNECTBAD_PROTOCOL | Broker từ chối phiên bản MQTT | Đổi sang MQTT 3.1.1 |
| 2 | MQTTCONNECTBADCLIENTID | Client ID trống hoặc không hợp lệ | Dùng client ID duy nhất |
| 3 | MQTTCONNECTUNAVAILABLE | Broker đang bận hoặc overload | Thử lại sau |
| 4 | MQTTCONNECTBAD_CREDENTIALS | Sai username/password | Kiểm tra credentials |
| 5 | MQTTCONNECTUNAUTHORIZED | Không có quyền kết nối | Kiểm tra ACL broker |
Thêm log mã lỗi vào code ngay từ đầu — tiết kiệm rất nhiều thời gian debug:
void printMqttState(int state) {
switch (state) {
case -4: Serial.println("MQTT: CONNECTION_TIMEOUT"); break;
case -3: Serial.println("MQTT: CONNECTION_LOST"); break;
case -2: Serial.println("MQTT: CONNECT_FAILED"); break;
case -1: Serial.println("MQTT: DISCONNECTED"); break;
case 1: Serial.println("MQTT: BAD_PROTOCOL"); break;
case 2: Serial.println("MQTT: BAD_CLIENT_ID"); break;
case 3: Serial.println("MQTT: UNAVAILABLE"); break;
case 4: Serial.println("MQTT: BAD_CREDENTIALS"); break;
case 5: Serial.println("MQTT: UNAUTHORIZED"); break;
}
}
Pattern Sai: Vòng While Blocking
Đây là code mà hầu hết tutorial dạy — và nó nguy hiểm trong production:
// ❌ ĐỪNG DÙNG TRONG DỰ ÁN THỰC
void reconnect() {
while (!mqtt.connected()) {
Serial.print("Connecting MQTT...");
if (mqtt.connect("ESP32Client")) {
Serial.println("connected");
mqtt.subscribe("home/cmd");
} else {
Serial.print("failed, rc=");
Serial.println(mqtt.state());
delay(5000); // ← CHẶN 5 GIÂY
}
}
}
Vấn đề: Nếu broker không lên được, code bị kẹt vô hạn trong vòng while. Trong thời gian đó:
- Cảm biến không đọc được
- WDT (Watchdog Timer) có thể reset ESP32
- Nút nhấn, relay, màn hình — không phản hồi gì
- Mất toàn bộ logic điều khiển
Pattern Đúng: Reconnect Với millis()
Thay vì chặn loop(), dùng millis() để thử reconnect theo chu kỳ mà không block:
const unsigned long MQTT_RECONNECT_INTERVAL = 5000; // 5 giây
unsigned long lastMqttAttempt = 0;
void handleMqttReconnect() {
if (mqtt.connected()) return;
unsigned long now = millis();
if (now - lastMqttAttempt < MQTT_RECONNECT_INTERVAL) return;
lastMqttAttempt = now;
Serial.printf("MQTT reconnect (state=%d)...\n", mqtt.state());
if (mqtt.connect("ESP32Client", MQTT_USER, MQTT_PASS)) {
Serial.println("MQTT connected");
mqtt.subscribe("home/cmd");
mqtt.subscribe("home/config/#");
} else {
Serial.printf("MQTT failed, state=%d\n", mqtt.state());
}
}
void loop() {
handleMqttReconnect(); // gọi mỗi loop, không block
if (mqtt.connected()) mqtt.loop();
// Các tác vụ khác vẫn chạy bình thường
readSensors();
updateDisplay();
handleButtons();
}
loop() vẫn chạy liên tục. Mỗi 5 giây, nếu MQTT chưa kết nối, code thử lại một lần rồi tiếp tục — không đợi, không chặn.
Xử Lý WiFi Và MQTT Cùng Lúc
Trong thực tế, WiFi mất trước MQTT mất sau. Phải xử lý đúng thứ tự — không thể reconnect MQTT khi WiFi chưa có:
enum ConnState { IDLE, WIFI_CONNECTING, MQTT_CONNECTING, CONNECTED };
ConnState connState = IDLE;
unsigned long lastAttempt = 0;
const unsigned long RETRY_INTERVAL = 5000;
void handleConnection() {
unsigned long now = millis();
switch (connState) {
case IDLE:
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi: connecting...");
WiFi.begin(WIFI_SSID, WIFI_PASS);
connState = WIFI_CONNECTING;
lastAttempt = now;
} else if (!mqtt.connected()) {
connState = MQTT_CONNECTING;
lastAttempt = 0; // thử ngay
} else {
connState = CONNECTED;
}
break;
case WIFI_CONNECTING:
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("WiFi OK: %s\n", WiFi.localIP().toString().c_str());
connState = MQTT_CONNECTING;
lastAttempt = 0;
} else if (now - lastAttempt > 15000) {
// 15 giây không được → thử lại từ đầu
Serial.println("WiFi timeout, retry");
WiFi.disconnect();
connState = IDLE;
lastAttempt = now;
}
break;
case MQTT_CONNECTING:
if (WiFi.status() != WL_CONNECTED) {
connState = IDLE; // WiFi mất → quay đầu
break;
}
if (now - lastAttempt < RETRY_INTERVAL) break;
lastAttempt = now;
if (mqtt.connect(CLIENT_ID, MQTT_USER, MQTT_PASS)) {
Serial.println("MQTT connected");
mqtt.subscribe("home/#");
connState = CONNECTED;
} else {
Serial.printf("MQTT failed (state=%d), retry in 5s\n", mqtt.state());
}
break;
case CONNECTED:
if (WiFi.status() != WL_CONNECTED || !mqtt.connected()) {
Serial.println("Connection lost");
connState = IDLE;
}
break;
}
}
void loop() {
handleConnection();
if (connState == CONNECTED) mqtt.loop();
// Logic chính luôn chạy
readSensors();
controlOutput();
}
State machine này xử lý đúng thứ tự ưu tiên: WiFi trước, MQTT sau, không bao giờ cố kết nối MQTT khi WiFi chưa có.
Exponential Backoff — Thử Lại Thông Minh Hơn
Thử lại cứ 5 giây một lần không phải lúc nào cũng tốt. Nếu broker đang quá tải, nhiều thiết bị retry cùng lúc sẽ làm broker nặng thêm. Exponential backoff tăng dần thời gian chờ:
unsigned long mqttRetryDelay = 1000; // bắt đầu 1 giây
const unsigned long MQTT_RETRY_MAX = 60000; // tối đa 60 giây
unsigned long lastMqttAttempt = 0;
void handleMqttReconnect() {
if (mqtt.connected()) {
mqttRetryDelay = 1000; // reset về 1s khi connected
return;
}
if (millis() - lastMqttAttempt < mqttRetryDelay) return;
lastMqttAttempt = millis();
if (mqtt.connect(CLIENT_ID, MQTT_USER, MQTT_PASS)) {
Serial.println("MQTT connected");
mqtt.subscribe("home/cmd");
mqttRetryDelay = 1000; // thành công → reset
} else {
// Thất bại → tăng gấp đôi, giới hạn ở max
mqttRetryDelay = min(mqttRetryDelay * 2, MQTT_RETRY_MAX);
Serial.printf("MQTT retry in %lus\n", mqttRetryDelay / 1000);
}
}
Lịch retry: 1s → 2s → 4s → 8s → 16s → 32s → 60s → 60s → …
Lưu Ý Thực Tế
Client ID Phải Duy Nhất
Nếu hai ESP32 dùng cùng client ID, broker sẽ ngắt kết nối cái cũ khi cái mới kết nối vào — gây vòng lặp disconnect liên tục:
// Dùng MAC address để đảm bảo unique
String clientId = "ESP32-" + WiFi.macAddress();
mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS);
Keep-Alive Phải Phù Hợp
PubSubClient mặc định keep-alive là 15 giây. Nếu loop() bị chặn hơn 15 giây (ví dụ: đọc SD card, OTA update), broker sẽ tự ngắt kết nối:
mqtt.setKeepAlive(60); // tăng lên 60 giây nếu cần
Và gọi mqtt.loop() đều đặn — ít nhất mỗi vài giây một lần.
Last Will và Testament (LWT)
Khai báo LWT để broker tự publish trạng thái offline khi thiết bị mất kết nối đột ngột — hữu ích cho dashboard giám sát:
// Khai báo LWT trước khi connect
mqtt.connect(
clientId.c_str(),
MQTT_USER,
MQTT_PASS,
"home/esp32/status", // topic LWT
0, // QoS
true, // retain
"offline" // message khi mất kết nối
);
// Sau khi connect thành công, publish online
mqtt.publish("home/esp32/status", "online", true);
Không Publish Khi Chưa Connected
Lỗi hay gặp: publish ngay sau mqtt.connect() mà không kiểm tra kết quả:
// ❌ Có thể publish vào void nếu connect thất bại
mqtt.connect(CLIENT_ID);
mqtt.publish("home/temp", "25.3");
// ✓ Kiểm tra trước
if (mqtt.connect(CLIENT_ID)) {
mqtt.publish("home/temp", "25.3");
}
So Sánh Các Pattern Reconnect
| Pattern | Ưu Điểm | Nhược Điểm | Dùng Khi Nào |
|---|---|---|---|
| While blocking | Code đơn giản | Block loop, WDT risk | Prototype/demo |
| millis() interval | Không block | Code hơi dài hơn | Dự án thực tế đơn giản |
| State machine | Xử lý đúng thứ tự WiFi→MQTT | Code phức tạp hơn | Production, nhiều thiết bị |
| Exponential backoff | Giảm tải broker | Thời gian reconnect dài hơn | Hệ thống nhiều node |
Checklist Trước Khi Deploy
- Client ID dùng MAC address — không hardcode string cố định
- Không có
while (!mqtt.connected())hoặcdelay()lớn trongloop() mqtt.loop()được gọi đều, không bị bỏ qua khi xử lý logic khác- Log
mqtt.state()khi kết nối thất bại — không log mù - Keep-alive phù hợp với thời gian tác vụ nặng nhất trong
loop() - LWT được khai báo cho dashboard giám sát trạng thái thiết bị
- Test offline: rút mạng → cắm lại → thiết bị tự reconnect không cần reset


