ESP32 heap and stack monitoring in the field
Most ESP32 devices that fail after three weeks fail for one of two reasons: the heap slowly ran out, or a task's stack was nearly full on the desk and overflowed in the field. Both are visible long before the crash, if you look at four numbers.
The four numbers
size_t free_now = esp_get_free_heap_size(); size_t free_minimum = esp_get_minimum_free_heap_size(); size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); UBaseType_t stack = uxTaskGetStackHighWaterMark(NULL); /* of this task, in bytes */
| Number | What it tells you |
|---|---|
| Free heap | How much is free right now. On its own it says little: it moves with every request. |
| Minimum free heap | The lowest the heap has been since start. This is your real margin: the worst moment so far. |
| Largest free block | The biggest single allocation that would succeed. When this is much smaller than the free heap, the heap is fragmented, and a large malloc can fail with plenty of memory free. |
| Stack high-water mark | The least free stack a task has ever had. Near zero means the next deeper call overflows it. |
Reading them over time
One reading is nearly useless. The shape over days is what finds the bug.
- Free heap steps down and never comes back: a leak. Look at what happened at each step; the log line next to it usually names it.
- Free heap is steady, largest block shrinks: fragmentation. Something allocates and frees blocks of mixed sizes. Allocate long-lived buffers once, at start.
- Minimum free heap drops once, sharply: one rare path needs much more memory than the rest, often a TLS handshake or a large JSON document.
- A task's free stack under about 300 bytes: enlarge that stack now. The mark only records the deepest call so far, and the field will find a deeper one.
Every task, not just yours
With CONFIG_FREERTOS_USE_TRACE_FACILITY, uxTaskGetSystemState() returns the high-water mark of every task, including the ones you did not write: the WiFi task, the TCP/IP task, the event loop, the HTTP server. Those overflow too, usually because a callback of yours runs on their stack.
Getting the numbers off the device
Printing them to the log every minute works on the desk. For devices in the field they have to travel, and they should be drawn as a line over time, because the shape is the information.
Pinscope reports all of these with no code: free heap, minimum and largest block, the least free stack of any task and of each task, WiFi signal, chip temperature and restarts by reason, once a minute, kept as history. Your own numbers are one call:
pinscope_metric("queue_depth", depth);
The component itself takes a fixed 9.4 KB of RAM, so it does not move the numbers it reports.
Before shipping
- Run the device for a few days under realistic load and look at the minimum free heap, not the current one.
- Check the free stack of every task after the worst case you can provoke: a full update, a large upload, a reconnect.
- Decide what the firmware does when an allocation fails. Returning an error is recoverable; a null pointer three calls later is a crash.
Try it
Three devices are free with every feature, and no card is needed. Create an account, or read the five-minute guide first.
More
- Remote access to an ESP32 without TLS: what it measured and what it costs
- Reaching an ESP32 web server from outside its network
- Updating ESP32 firmware over the internet, with rollback
- Crash backtraces from devices in the field
- Remote logging: ESP_LOG without a serial cable
- An ESP32 behind CGNAT, 4G or Starlink
- TLS memory usage on the ESP32, and how to reduce it