If your ESP32 is crashing after a few hours of running mqtt.publish(), you are likely chasing a ghost. The arduino esp32 mqtt.publish memory leak is rarely a flaw in the MQTT library itself. In 90% of bench cases, it is a heap fragmentation issue triggered by blocking local sensor buses or improper payload string handling. When a local hardware bus hangs, the ESP32's main loop stalls, the MQTT client queue backs up, and poorly managed memory allocations tear the heap apart until the watchdog resets the chip.
To fix this, we must look at the entire data pipeline: from the physical sensor bus wiring up to the C++ payload builder. Here is the exact diagnostic path and the code to make your node run for months without a reboot.
The Physical Layer: Sensor Bus Mechanics and Wiring
Before data reaches the MQTT broker, it must be read from local sensors. Choosing the wrong bus or wiring it incorrectly is the root cause of the blocking behavior that triggers memory leaks. Below is the decision matrix for selecting the right protocol based on distance, speed, and device count.
| Protocol | Wires | Max Speed | Addressing | Max Distance | Best For |
|---|---|---|---|---|---|
| I2C | 2 (SDA, SCL) | 3.4 MHz (Ultra-Fast) | 7-bit / 10-bit I2C Address | ~1 meter (bus capacitance limit) | On-board environmental sensors (BME280, SHT31) |
| UART | 2 (TX, RX) | 1 Mbps (typical) | None (Point-to-Point) | ~15 meters (RS-485 extends this) | GPS modules, PM2.5 sensors, cellular modems |
| SPI | 4 (MOSI, MISO, SCK, CS) | 50+ MHz | Hardware Chip Select (CS) pins | ~30 cm (signal integrity limit) | High-throughput (TFT displays, SD cards) |
| CAN | 2 (CANH, CANL) | 1 Mbps (Classic) | 11-bit / 29-bit Arbitration ID | 40m @ 1Mbps / 1km @ 50kbps | Automotive, noisy industrial environments |
Physical Wiring and Pull-Up Requirements
I2C is the most common bus for environmental sensors, and it is an open-drain architecture. This means the ESP32 and the sensor can only pull the SDA and SCL lines low. To pull them high, you must provide external pull-up resistors. For a standard 100 kHz I2C bus with a few sensors, use 4.7kΩ resistors tied to 3.3V. If you push to 400 kHz Fast Mode, drop to 2.2kΩ to overcome bus capacitance (NXP I2C Specification UM10204).
The Classic Failures
When your ESP32 randomly freezes, check for these three hardware-level culprits:
- Missing pull-up: Without resistors, the SDA/SCL lines float. The
Wire.requestFrom()function will wait indefinitely for a clock edge that never comes, permanently blocking the FreeRTOS task. - Address clash: Connecting two BME280 sensors without changing the SDO pin state leaves both at I2C address
0x76. The bus will collide, corrupt data, and occasionally lock up the I2C state machine. - Baud mismatch: On UART, setting the ESP32 to 115200 baud while a GPS module defaults to 9600 baud results in garbage characters. If your parsing logic relies on a specific string terminator that never arrives, your buffer overflows.
Why Hardware Blocks Trigger the MQTT Memory Leak
How does a floating I2C line cause an mqtt.publish memory leak? It comes down to how the Arduino framework and the PubSubClient library manage the main loop and the heap.
When you call mqtt.publish(topic, payload), the library doesn't always transmit immediately. If the Wi-Fi radio is busy, or if the broker hasn't acknowledged the previous packet, the library queues the message in RAM. If your main loop is blocked by a hanging I2C read, the mqtt.loop() function isn't called. The MQTT keep-alive timer expires, the broker drops the TCP connection, and the ESP32 attempts to reconnect. During this chaotic state, queued messages pile up.
The primary driver of the arduino esp32 mqtt.publish memory leak is building JSON payloads using the Arduino String class. Every time you concatenate a String (e.g., payload += String(temperature);), the microcontroller allocates a new block of memory on the heap and abandons the old one. Over thousands of publish cycles, this leaves "Swiss cheese" gaps in the heap. Eventually, a contiguous block large enough for the next payload cannot be found, malloc() fails, and the ESP32 panics.
According to Espressif's Heap Debugging documentation, monitoring heap fragmentation is critical for long-running IoT nodes. You must replace dynamic String allocations with fixed-size char arrays and use snprintf to format your MQTT payloads.
Sniffing the Bus and Debugging the Heap
To prove whether your issue is a physical bus hang or a software memory leak, you need to instrument both layers.
How to Sniff and Debug the Hardware Bus
Do not guess if your I2C pull-ups are working; verify them. Connect a $15 USB logic analyzer (like a Saleae clone) to SDA and SCL. Use open-source software like PulseView/Sigrok to decode the I2C traffic. Trigger on the SDA falling edge. If you see the ESP32 send a read command but the 9th clock cycle (the ACK bit) stays high, the sensor is not responding. This confirms a missing pull-up, a blown sensor, or an address clash.
Minimal Working Exchange: Non-Blocking & Leak-Free
Below is a complete, robust pattern for reading an I2C sensor and publishing via MQTT. It avoids blocking delays, uses fixed memory buffers, and tracks heap health (PubSubClient API Reference).
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
// --- Pin & Network Definitions ---
#define I2C_SDA 21
#define I2C_SCL 22
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASS";
const char* mqtt_server = "192.168.1.100";
WiFiClient espClient;
PubSubClient mqtt(espClient);
Adafruit_BME280 bme;
// Fixed-size buffer prevents heap fragmentation
char payload_buffer[128];
unsigned long lastPublish = 0;
unsigned long lastHeapCheck = 0;
void setup_wifi() {
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void reconnect_mqtt() {
while (!mqtt.connected()) {
if (mqtt.connect("ESP32_Sensor_Node")) {
mqtt.subscribe("cmd/topic");
} else {
delay(5000); // Wait 5s before retrying
}
}
}
void setup() {
Serial.begin(115200);
setup_wifi();
mqtt.setServer(mqtt_server, 1883);
// Explicit I2C pin mapping and 400kHz fast-mode setup
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(400000);
// Verify sensor presence to prevent Wire hangs
if (!bme.begin(0x76, &Wire)) {
Serial.println("FATAL: BME280 not found. Check pull-ups and address.");
while (1) { delay(1000); } // Halt safely
}
}
void loop() {
if (!mqtt.connected()) {
reconnect_mqtt();
}
mqtt.loop(); // MUST be called frequently to process keep-alives
unsigned long now = millis();
// Non-blocking publish interval (every 10 seconds)
if (now - lastPublish > 10000) {
lastPublish = now;
float temp = bme.readTemperature();
float hum = bme.readHumidity();
// Use snprintf into a fixed char array (NO String objects)
snprintf(payload_buffer, sizeof(payload_buffer),
"{\"temp\":%.2f,\"hum\":%.1f,\"heap\":%lu}",
temp, hum, ESP.getFreeHeap());
mqtt.publish("sensor/bme280", payload_buffer);
}
// Monitor heap health every 60 seconds
if (now - lastHeapCheck > 60000) {
lastHeapCheck = now;
Serial.printf("Free Heap: %u bytes | Min Ever: %u bytes\n",
ESP.getFreeHeap(), ESP.getMinFreeHeap());
}
}
By enforcing fixed-size memory buffers and ensuring the physical I2C layer is properly terminated with pull-up resistors, you eliminate the two primary vectors for the mqtt.publish memory leak. Your ESP32 will maintain a stable heap baseline, and the minimum free heap metric will stop degrading over time.






