iiOTESP32 Brasil
Contingency mode: public pages and downloads remain available; login, community, and server data may be unavailable.
Back to tutorial catalog
Ilustração: placa ESP32 com sensores de temperatura, umidade e planta enviando dados pelo Wi-Fi a um servidor em nuvem, que se comunica com um laptop mostrando gráficos
Imagem ilustrativa gerada por IA
IntermediateMQTTReading: 13 min

MQTT automatic reconnection without blocking the loop

The while loop found in every tutorial freezes the firmware when the broker goes down. Learn the millis() guard pattern, separate schedules and rc codes.

Technologies:#ESP32#MQTT#PubSubClient#millis#Last Will

Hardware requirements:

  • Placa ESP32 ou ESP32-S3
  • Rede Wi-Fi de 2,4 GHz
  • Um broker MQTT: Mosquitto local, HiveMQ ou equivalente

The loop every tutorial teaches, and you should not use

Search for MQTT reconnection on the ESP32 and you will find some variation of this:

code.cpp
cpp
1while (!client.connected()) {
2 Serial.print("Conectando ao broker...");
3 if (client.connect("esp32")) {
4 Serial.println("conectado");
5 } else {
6 Serial.print("falhou, rc=");
7 Serial.println(client.state());
8 delay(5000);
9 }
10}

It works on the bench, with the broker on the same computer. In the field, it is a trap.

While this loop runs, the entire firmware is stopped. The web server does not respond, the physical button is not read, Modbus is not polled and the process state machine does not advance. If the broker is down for ten minutes, your device is dead for ten minutes — not disconnected, dead, doing nothing except trying and sleeping for five seconds.

Worse, in many projects this feeds the watchdog irregularly and the ESP32 restarts by itself. The diagnosis then becomes "the device is rebooting", and nobody connects the reset to the broker outage.

The inversion: a guard instead of a loop

The correct pattern has no loop at all. Call the connection function on every pass through loop(), and let it decide whether it needs to do anything:

code.cpp
cpp
1void connectMqtt() {
2 if (config.mqttHost.isEmpty() || mqtt.connected() || WiFi.status() != WL_CONNECTED) {
3 return;
4 }
5 if (millis() - lastMqttAttemptMs < 5000) {
6 return;
7 }
8 lastMqttAttemptMs = millis();
9 mqtt.setServer(config.mqttHost.c_str(), config.mqttPort);
10 mqtt.setCallback(onMqttMessage);
11 if (mqtt.connect(config.deviceId.c_str(), config.deviceId.c_str(), config.deviceToken.c_str())) {
12 mqtt.subscribe(topicFor("commands").c_str());
13 } else {
14 Serial.printf("Falha MQTT rc=%d\\n", mqtt.state());
15 }
16}

Read the two guards carefully, because they contain the entire difference.

The first gives up in three cases: no broker configured, already connected or Wi-Fi down. Trying MQTT without a network is guaranteed waste — the call blocks until the socket timeout expires for no reason.

The second is the heart of the pattern. If the last attempt was less than five seconds ago, it returns immediately. This replaces delay(5000) without stopping the rest of the firmware: instead of sleeping for five seconds, the function returns in microseconds and loop() continues doing everything else. Once five seconds have passed, the next pass finds the guard open and tries again.

The effect is the same — one attempt every five seconds — but the cost is zero.

Wi-Fi and MQTT have different cadences on purpose

Notice that Wi-Fi reconnection uses the same structure with a much longer interval:

code.cpp
cpp
1void ensureWiFi() {
2 if (config.networkMode == "ap") {
3 return;
4 }
5 if (WiFi.status() == WL_CONNECTED) {
6 return;
7 }
8 if (millis() - lastWifiAttemptMs < 30000) {
9 return;
10 }
11 lastWifiAttemptMs = millis();
12 connectWiFiOnce(5000);
13}

Thirty seconds for Wi-Fi, five for MQTT. This is not arbitrary.

Reassociating with Wi-Fi turns on the radio, scans, negotiates authentication and waits for DHCP. It costs energy and takes time. Trying every five seconds while the router is off wastes battery and does not speed anything up.

An MQTT connection is a TCP handshake over a network that is already up. It is cheap. And when the broker returns, you want to reconnect quickly because every disconnected second is telemetry that does not get published.

The general rule is: the more expensive the operation, the longer the interval between attempts.

The order inside loop matters

code.cpp
cpp
1void loop() {
2 handleSerialCommands();
3 webServer.handleClient();
4 updateConfigAccessPoint();
5 ensureWiFi();
6 connectMqtt();
7 if (mqtt.connected()) {
8 mqtt.loop();
9 }
10 runProgramTick();
11 unsigned long now = millis();
12 if (now - lastTelemetryMs >= 10000) {
13 lastTelemetryMs = now;
14 sendTelemetry();
15 }
16}

Wi-Fi comes before MQTT because the second depends on the first. And mqtt.loop() is called only when there is a connection, protected by the if.

This mqtt.loop() is mandatory and frequently forgotten. It processes incoming messages and, most importantly, answers the broker's keepalive. Without calling it regularly, the broker considers the client dead and drops the connection — creating a reconnection cycle that looks like a network problem but is a code problem.

Also notice that there is no delay anywhere in the loop. All scheduling is done by comparing millis().

The rc codes and what each one means

When the connection fails, mqtt.state() returns a number. It is the most useful diagnostic information, and almost every tutorial prints it without explaining it:

code.cpp
cpp
1-4 timeout: broker unreachable, wrong IP or port
2-3 connection lost: network dropped in the middle
3-2 connection failed: DNS did not resolve or TCP was refused
4-1 disconnected by a local call
5 0 connected
6 1 protocol version rejected by the broker
7 2 client id rejected
8 3 broker unavailable
9 4 wrong username or password
10 5 not authorized

The practical reading is: negative values mean a network or address problem; positive values mean broker rejection, meaning the broker received your connection and said no.

-2 is usually a wrong hostname. 4 and 5 are credentials and, with token-authenticated brokers, almost always an expired token. 2 appears when the client id contains a character the broker does not accept or is too long.

Duplicate client ids and the false network diagnosis

This deserves its own section because it produces a misleading symptom.

The MQTT protocol requires every client to have a unique identifier. If two devices connect with the same id, the broker drops the first to accept the second. The first reconnects and drops the second, and so on forever.

The result is a pair of devices that repeatedly connects and drops, with no log error beyond disconnections. It is easy to confuse with unstable Wi-Fi.

The firmware above prevents it: the client id is deviceId, which is unique per device. If you use a literal such as "esp32" — the value found in almost every example — the problem appears the day the second board goes into operation.

If there is no natural identifier, derive one from the MAC address, which is unique at the factory.

Last Will: announce that it died

There is one case reconnection cannot solve: the device loses power. It cannot notify anyone, and the system on the other side keeps showing the last received value as if it were current.

MQTT solves this with the Last Will and Testament. At connection time, register a message that the broker publishes for you if the connection drops without a goodbye:

code.cpp
cpp
1mqtt.connect(clientId, usuario, senha,
2 topicoStatus, 1, true, "offline");

The extra parameters are the will topic, quality of service, retained flag and message. As soon as it connects, publish online to the same topic, also retained:

code.cpp
cpp
1mqtt.publish(topicoStatus, "online", true);

The result is a reliable presence topic. While the device is alive it says online; if it disappears, the broker itself publishes offline after the keepalive expires. The system on the other side can now distinguish old data from a fallen device, which is a distinction that saves a support shift.

This tutorial's firmware does not use Last Will yet — it is a worthwhile improvement, and the kit includes a ready example.

Tutorial: from zero to automatic reconnection

Step 1: prepare the project

Create the project in PlatformIO and declare the only external dependencies:

code.cpp
cpp
1[env:esp32-mqtt]
2platform = espressif32
3board = esp32dev
4framework = arduino
5monitor_speed = 115200
6lib_deps =
7 knolleary/PubSubClient@^2.8
8 bblanchon/ArduinoJson@^7.2.1

Step 2: adjust the parameters

At the top of the kit firmware, provide the network, broker and device identifier. Use a different identifier on every board — that is what prevents the drop cycle described above.

If your broker does not require authentication, leave the username and password empty.

Step 3: flash and observe normal behavior

code.cpp
cpp
1pio run -t upload
2pio device monitor -b 115200

You should see the Wi-Fi connection, then the MQTT connection, and telemetry arriving at a regular interval. Up to here, this is the easy case.

Step 4: the important test — take down the broker

With everything working, stop the broker. If it is a local Mosquitto instance, stop the service.

Watch the serial monitor. The correct behavior is: the device detects the failure, tries every five seconds, prints the rc for each attempt, and continues doing everything else — telemetry keeps being generated and the web server keeps responding.

If the firmware freezes here, the guard pattern has not been applied.

Step 5: bring the broker back

Restart the broker and observe it. Reconnection should happen within five seconds, without intervention and without restarting the board.

Step 6: take down Wi-Fi

Now turn off the router or access point. The WiFi.status() guard should prevent MQTT attempts while there is no network — you should not see a stream of negative rc values, but MQTT silence while Wi-Fi tries every thirty seconds.

Turn the router back on. The correct sequence is Wi-Fi first, MQTT immediately after.

Step 7: verify keepalive

Leave it running for twenty minutes without publishing anything. If the connection drops by itself during that interval, mqtt.loop() is not being called regularly — look for a long delay in your loop.

Common errors and solutions

The device freezes when the broker goes down. There is still a while loop or a delay in the connection routine. Apply the guard pattern.

It reconnects and drops in a cycle, with no clear error. Duplicate client id. Confirm that every board uses a different identifier.

It drops by itself after a few minutes while idle. mqtt.loop() is not being called regularly, or a long delay is blocking keepalive.

Constant rc=-2. The broker name does not resolve. Test with the IP directly to separate a DNS problem from an address problem.

rc=4 or rc=5. Credentials. With a token-based broker, check validity before suspecting the code.

It publishes but receives nothing. The topics were not subscribed to after reconnecting. A subscription does not survive a disconnect, which is why subscribe is inside the successful connect block.

It works with one device and fails with several. Besides the client id, check the broker connection limit and the PubSubClient buffer size, which is small by default and truncates larger messages.

See more technical tutorialsIIoT ESP32 Brasil — Practical documentation