Memory leaks on microcontrollers are usually heap leaks (malloc/new without free/delete) or resource leaks (OS objects, buffers, drivers). Many embedded projects avoid the heap, so the “best” method depends on whether you use bare-metal or an RTOS.
1) Fast ways to detect leaks (works on most MCUs)
A) Track heap usage over time (high signal, low effort)
If you use malloc/free (or new/delete), periodically sample:
- current free heap
- minimum ever free heap (watermark)
- largest free block (fragmentation indicator)
Red flag: free heap steadily decreases after repeated operations (e.g., every reconnect, every command, every loop).
How:
- FreeRTOS: xPortGetFreeHeapSize(), xPortGetMinimumEverFreeHeapSize()
- newlib (common GCC embedded): mallinfo() / mallinfo2() (availability varies)
- Vendor libs sometimes provide heap stats.
B) Add a “heap watermark” canary (bare-metal friendly)
If you own the heap region (or a big static arena), fill it with a pattern at boot (e.g., 0xA5) and later scan to see how much got overwritten.
Useful when: you don’t have good heap introspection, or you use a custom allocator.
C) Instrument allocations (most reliable)
Wrap malloc/free (and new/delete) to log:
- pointer
- size
- callsite (file/line) or return address
- running totals
Red flag: allocations not matched by frees after a test scenario.
Minimal embedded approach:
- keep a small ring buffer of recent alloc/free events
- keep counters: total alloc bytes, total freed bytes, outstanding allocations
D) Look for stack growth vs heap collision (classic embedded failure)
On MCUs where heap grows up and stack grows down, a “leak” can look like random crashes.
Monitor:
- stack high-water mark per task (RTOS) or main stack usage (bare-metal)
- heap end pointer
2) If you’re on an RTOS (FreeRTOS example)
Leak-like symptoms often come from:
- tasks created repeatedly and not deleted
- queues/semaphores/timers created repeatedly
- network stack buffers not returned
Check:
- uxTaskGetStackHighWaterMark() per task (stack overflow can mimic leaks)
- xPortGetMinimumEverFreeHeapSize() (heap watermark)
- Ensure every create has a corresponding delete where appropriate:
vTaskDelete(), vQueueDelete(), vSemaphoreDelete(), etc.
Best practice: create OS objects once at startup; don’t create/destroy repeatedly in runtime loops.
3) If you’re bare-metal (no RTOS)
A) Avoid heap entirely (most robust)
Use:
- static buffers
- memory pools
- fixed-size ring buffers
B) If you must use heap: add hard guards
- forbid dynamic allocation after init (“heap lock”)
- assert if malloc/new is called after a certain point
- use a pool allocator with fixed blocks
4) Practical test plan (what you actually run)
Pick a scenario that repeats:
- connect/disconnect Wi-Fi (or BLE)
- open/close a socket
- parse a command message
- start/stop a sensor sampling mode
Then run:
- log heap free + min-ever-free every N cycles
- run 1,000–100,000 cycles
- compare start vs end
If end free heap is lower and never returns, you likely have a leak (or fragmentation).
5) Fragmentation vs leak (important distinction)
A microcontroller can “run out of memory” even if you free everything, because of fragmentation.
Signs of fragmentation:
- free heap looks “okay”, but allocations fail
- “largest free block” keeps shrinking
- many different allocation sizes over time
Fixes:
- allocate same-sized blocks (pool)
- avoid frequent allocate/free of variable sizes
- reuse buffers
6) Tooling options (when you can use them)
- Host-side tests: compile parts of code for PC and run ASan/LSan/Valgrind (great for finding logic leaks in parsers/protocol code)
- RTT/UART logging: stream allocation logs off-device
- Linker map review: ensure you’re not confusing BSS growth with heap issues
On-target Valgrind/ASan is typically not feasible for small MCUs, but host simulation often catches the same bug patterns.
7) Common embedded leak sources (checklist)
- String usage (Arduino-style) causing heap churn/fragmentation
- JSON parsing libs allocating internally
- networking stacks (TCP reconnect path)
- repeated “init” functions that allocate but don’t deinit
- new in constructors without matching delete (or objects stored but never released)
- error paths that return early without freeing

Top comments (0)