Back to Blog
Embedded Systems

Memory Management in Embedded Firmware: Avoiding Heap Fragmentation and Stack Overflows

Embedded systems have kilobytes of RAM, not gigabytes. Memory errors — heap fragmentation, stack overflows, memory leaks — are silent killers that manifest as random crashes in production. Here is how to engineer memory robustly.

June 10, 2024
14 min read
Memory ManagementEmbedded SystemsFreeRTOSESP32

Memory Management in Embedded Firmware: Avoiding Heap Fragmentation and Stack Overflows

Memory errors are the most common cause of mysterious production failures in embedded firmware. A device works perfectly in testing for weeks, then starts crashing randomly after 72 hours of production operation. The cause is almost always heap fragmentation, a stack overflow, or a memory leak. These bugs are hard to reproduce and harder to diagnose without the right tools and mindset.

This guide gives you the framework to design firmware that never runs out of memory.

Static vs Dynamic Allocation: The Core Trade-Off

The safest embedded firmware allocates all memory statically at compile time. If it compiles and links with your memory budget, it will never run out of memory at runtime.

// Static allocation — deterministic, no fragmentation possible
static uint8_t  uart_rx_buffer[1024];
static uint8_t  mqtt_payload_buffer[512];
static sensor_reading_t reading_history[100];

// Dynamic allocation — flexible, but risks fragmentation uint8_t *buf = malloc(1024); // May fail after extended runtime if (!buf) handle_oom_error();

For production embedded firmware, the rule is: prefer static allocation everywhere possible, use dynamic allocation only where flexibility is essential (e.g., variable-length MQTT messages, dynamic JSON parsing, plugin architectures).

Heap Fragmentation: How It Happens and How to Fix It

Heap fragmentation occurs when allocations and frees of different sizes leave the heap as a patchwork of small free blocks. Your firmware can have 10 KB of total free heap but be unable to allocate a single 5 KB block because no contiguous free region of that size exists.

// Fragmentation demonstration (bad pattern — do not do this)
void process_messages(void) {
    for (;;) {
        // Alternating small and large allocations
        char *small = malloc(64);    // Free block A: 64 bytes
        char *large = malloc(2048);  // Free block B: 2048 bytes
        process(small, large);
        free(large);  // Free B — 2048-byte hole
        free(small);  // Free A — 64-byte hole (may not merge with B if not adjacent)
        // After 1000 iterations: heap is swiss cheese
    }
}

Fix 1: Memory Pool Allocator

A memory pool pre-allocates a fixed number of fixed-size blocks. Allocation is O(1) and fragmentation-free. The trade-off is that block size must be chosen at design time.

#define POOL_BLOCK_SIZE   256
#define POOL_BLOCK_COUNT  16

typedef struct pool_block { struct pool_block *next; uint8_t data[POOL_BLOCK_SIZE]; } pool_block_t;

static pool_block_t pool_storage[POOL_BLOCK_COUNT]; static pool_block_t *pool_free_list = NULL;

void pool_init(void) { for (int i = 0; i < POOL_BLOCK_COUNT - 1; i++) { pool_storage[i].next = &pool_storage[i + 1]; } pool_storage[POOL_BLOCK_COUNT - 1].next = NULL; pool_free_list = &pool_storage[0]; }

void *pool_alloc(void) { if (!pool_free_list) return NULL; // Pool exhausted pool_block_t *block = pool_free_list; pool_free_list = block->next; return block->data; }

void pool_free(void *ptr) { pool_block_t *block = (pool_block_t *)((uint8_t *)ptr - offsetof(pool_block_t, data)); block->next = pool_free_list; pool_free_list = block; }

This pool allocates 256-byte blocks in O(1) with zero fragmentation. Use separate pools for different size classes (64-byte, 256-byte, 1024-byte) to cover different allocation patterns.

FreeRTOS Heap Implementations

FreeRTOS provides five heap implementations. Choose based on your requirements:

| Heap | Supports free() | Fragmentation-safe | Use case | |---|---|---|---| | heap_1 | No | Yes (static) | Safety-critical, no dynamic dealloc | | heap_2 | Yes | No | Simple, same-size reuse patterns | | heap_3 | Yes | System malloc | Wraps newlib malloc (external) | | heap_4 | Yes | Merges adjacent | General purpose — most common | | heap_5 | Yes | Merges adjacent | Multiple non-contiguous memory regions |

// Configure heap_4 in FreeRTOSConfig.h
#define configUSE_HEAP_4  1
#define configTOTAL_HEAP_SIZE  (50 * 1024)  // 50 KB

// Monitor heap health in production void log_heap_status(void) { ESP_LOGI("MEM", "Free heap: %lu bytes, min ever: %lu bytes", xPortGetFreeHeapSize(), xPortGetMinimumEverFreeHeapSize()); }

Log xPortGetMinimumEverFreeHeapSize() periodically. If this watermark is below 5 KB, you are close to exhaustion under peak load.

Stack Sizing Strategy

Every FreeRTOS task needs a stack. Too small: stack overflow, corrupt neighboring memory, random crashes. Too large: wasted RAM. Size stacks correctly with these steps:

Step 1: Start tasks with 4096-byte stacks (safe default).

Step 2: During development, log the high watermark from each task:

void sensor_task(void *pvParameters) {
    for (;;) {
        // ... task work

// Log every 60 seconds static uint32_t last_log = 0; if (xTaskGetTickCount() - last_log > pdMS_TO_TICKS(60000)) { last_log = xTaskGetTickCount(); UBaseType_t watermark = uxTaskGetStackHighWaterMark(NULL); ESP_LOGI("STACK", "sensor_task watermark: %u words remaining", watermark); } } }

Step 3: Production stack size = (configured size - watermark) × 1.5, rounded up to 256-byte boundary.

If your task shows 512 words (2048 bytes) remaining from a 4096-byte stack, it used 2048 bytes peak. Production size = 2048 × 1.5 = 3072 bytes.

ESP32 SPIRAM (PSRAM) for Large Allocations

ESP32-WROVER modules include 8 MB of SPI-connected PSRAM. This is slower than internal SRAM (longer access latency, cache-assisted) but sufficient for large buffers: HTTP response bodies, image buffers, TLS session caches.

#include "esp_heap_caps.h"

// Allocate in SPIRAM — slower but 8MB available uint8_t *large_buffer = heap_caps_malloc(65536, MALLOC_CAP_SPIRAM); if (!large_buffer) { ESP_LOGE("MEM", "Failed to allocate 64 KB from SPIRAM"); return ESP_ERR_NO_MEM; }

// Allocate in internal SRAM — faster, latency-sensitive code paths uint8_t *fast_buffer = heap_caps_malloc(1024, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);

// Check SPIRAM availability at boot size_t spiram_size = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); ESP_LOGI("MEM", "SPIRAM available: %u bytes", spiram_size);

Rules for SPIRAM:

  • Use for large infrequently-accessed buffers (>16 KB)
  • Never place ISR stack or FreeRTOS task stacks in SPIRAM
  • Never use SPIRAM for time-critical data accessed in tight loops
  • DMA operations cannot access SPIRAM directly — must copy to internal RAM first
  • Memory Leak Detection

    Memory leaks in embedded firmware are subtle — you may not notice them for days. Pattern: free heap decreases monotonically over days of operation until the device crashes.

    // Wrapper with leak tracking for development builds
    #ifdef DEBUG_MEMORY_LEAKS
    #define MAX_ALLOC_RECORDS 256

    typedef struct { void *ptr; size_t size; const char *file; int line; } alloc_record_t;

    static alloc_record_t alloc_records[MAX_ALLOC_RECORDS]; static int alloc_count = 0;

    void *debug_malloc_tracked(size_t size, const char *file, int line) { void *ptr = malloc(size); if (ptr && alloc_count < MAX_ALLOC_RECORDS) { alloc_records[alloc_count++] = (alloc_record_t){ptr, size, file, line}; } return ptr; }

    #define malloc(s) debug_malloc_tracked(s, __FILE__, __LINE__) #endif

    In production (release build), this is compiled away. In development, you can dump alloc_records over serial to see what is currently allocated and where it was allocated from.

    For related guidance on structuring tasks that manage memory safely, see our [FreeRTOS task scheduling guide](/freertos-esp32-task-scheduling).

    [Contact Code Caracal](/contact) — we build production firmware for clients across 15+ countries.

    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    More Articles

    Business · 12 min read

    IoT Device Compliance: FCC, CE, and Product Certification Guide for Hardware Startups

    Business · 11 min read

    What to Look for When Hiring an IoT Development Partner: 8 Critical Criteria

    Business · 11 min read

    IoT MVP to Production: Realistic Timeline and Budget for Hardware Startups

    Business · 11 min read

    IoT Development Agency vs Building In-House: A Decision Framework for Founders

    IoT Dashboard · 13 min read

    Next.js IoT Analytics Dashboard: From Sensor Data to Production App

    Business · 11 min read

    How Much Does It Cost to Build an IoT Product in 2024? A Realistic Breakdown

    IoT Dashboard · 11 min read

    IoT Dashboard UX: Design Principles for Industrial Monitoring Interfaces

    IoT Dashboard · 12 min read

    Node.js WebSocket Server: The Real-Time Backend for IoT Dashboards

    Cloud & DevOps · 12 min read

    Containerizing IoT Backend Services with Docker: From Dev to Production

    IoT Dashboard · 14 min read

    Grafana + InfluxDB IoT Monitoring: Complete Production Setup Guide

    IoT Dashboard · 12 min read

    Building Real-Time IoT Dashboards with React and Recharts

    Cloud & DevOps · 13 min read

    CI/CD for Embedded Firmware: Automated Build, Test, and OTA Release Pipeline

    Mobile Development · 12 min read

    Flutter Offline-First IoT Apps: Hive + Sync Architecture That Works in the Field

    Cloud & DevOps · 14 min read

    Terraform for IoT Infrastructure: Provisioning AWS IoT Core, Lambda, and InfluxDB as Code

    Mobile Development · 10 min read

    Flutter IoT Alerts: Firebase Push Notifications for Device Events

    Cloud & DevOps · 12 min read

    Deploying IoT Backends on AWS: ECS Fargate vs Lambda vs EC2 Decision Guide

    Mobile Development · 11 min read

    Flutter + MQTT: Building Production IoT Mobile Apps That Scale

    Mobile Development · 13 min read

    Flutter BLE: Building a Bluetooth IoT Controller App from Scratch

    Cloud & DevOps · 13 min read

    AWS IoT Core vs Azure IoT Hub vs Google Cloud IoT: 2024 Honest Comparison

    IoT Engineering · 13 min read

    Kafka vs RabbitMQ for IoT: Choosing the Right Message Queue for High-Volume Telemetry

    IoT Engineering · 14 min read

    IoT System Testing: Unit, Integration, Hardware-in-the-Loop, and End-to-End

    IoT Engineering · 14 min read

    Predictive Maintenance with IoT Sensor Data: From Threshold to Machine Learning

    Embedded Systems · 14 min read

    IoT Bootloader Design: Secure Boot, A/B Partitions, and Reliable OTA Recovery

    IoT Engineering · 14 min read

    Multi-Tenant IoT Platform Architecture: Isolation, Scaling, and Data Partitioning

    IoT Engineering · 13 min read

    IoT Cost Optimization: How We Cut AWS IoT Bills by 60% Without Sacrificing Reliability

    IoT Engineering · 12 min read

    Edge Computing in IoT: When to Process On-Device vs In the Cloud

    IoT Engineering · 13 min read

    Digital Twins for IoT: Building a Virtual Mirror of Your Physical Devices

    Embedded Systems · 14 min read

    ESP32 Deep Sleep Mastery: Cutting Power Consumption from 240mA to 10µA

    IoT Engineering · 10 min read

    MQTT QoS 0, 1, and 2 Explained: Choosing the Right Level for IoT

    IoT Engineering · 14 min read

    IoT Monitoring and Observability: Metrics, Logs, and Distributed Tracing

    Embedded Systems · 14 min read

    Debugging Embedded Firmware: JTAG, GDB, Logic Analyzers, and Serial Tracing

    IoT Engineering · 12 min read

    WebSocket vs MQTT vs Server-Sent Events: Real-Time IoT Protocol Deep Dive

    Embedded Systems · 13 min read

    STM32 HAL vs Low-Level Drivers: When the Abstraction Costs You Too Much

    IoT Engineering · 13 min read

    IoT Data Pipeline: From Raw Sensor Reading to Live Dashboard in Under 100ms

    IoT Engineering · 13 min read

    Zero-Touch IoT Device Provisioning: Scaling from 10 to 100,000 Devices

    Embedded Systems · 13 min read

    UART vs SPI vs I2C: Choosing the Right Protocol for Sensor Integration

    IoT Engineering · 12 min read

    Real-Time IoT Alerting: From Simple Thresholds to ML Anomaly Detection

    Embedded Systems · 12 min read

    ESP32 Partition Table: Designing Flash Layout for Production Firmware

    IoT Engineering · 12 min read

    IoT Architecture Patterns: Hub-and-Spoke, Mesh, and Edge-Cloud Hybrid

    Embedded Systems · 13 min read

    IoT Battery Life Optimization: Engineering Devices That Last Years on a Single Charge

    IoT Engineering · 13 min read

    Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

    Security · 14 min read

    Zero-Trust Security for Embedded IoT: Why Your Devices Are Probably Vulnerable

    Embedded Systems · 14 min read

    FreeRTOS on ESP32: Task Scheduling, Queues, and Resource Management for IoT

    IoT Engineering · 12 min read

    Building a Production IoT Gateway with Raspberry Pi and Node.js

    Embedded Systems · 13 min read

    ESP32 vs STM32: Choosing the Right Microcontroller for Your IoT Project

    Mobile Development · 10 min read

    Flutter + WebSocket: Building Real-Time IoT Dashboards That Don't Stutter

    IoT Engineering · 13 min read

    IoT Fleet Management at Scale: AWS IoT Core Device Registry and Provisioning

    IoT Engineering · 11 min read

    MQTT vs HTTP for IoT: Which Protocol Wins in Production?

    IoT Engineering · 12 min read

    ESP32 → MQTT → AWS IoT Core: The Production-Grade Architecture Guide

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options