How Does FreeRTOS Heap Memory Management Work?
Last updated 1 July 2026 · 11 min read
Direct Answer
FreeRTOS ships five heap allocators — heap_1 through heap_5 — as interchangeable source files in the kernel's MemMang directory. heap_4 is the recommended choice for most embedded projects: it is a thread-safe coalescing best-fit allocator over a single static array whose size you set with configTOTAL_HEAP_SIZE in FreeRTOSConfig.h, and it recombines freed blocks to prevent fragmentation from accumulating over time. All dynamic allocation passes through pvPortMalloc() and vPortFree(); monitor remaining free space with xPortGetFreeHeapSize() and xPortGetMinimumEverFreeHeapSize(), and enable configUSE_MALLOC_FAILED_HOOK to catch allocation failures at runtime. For safety-critical firmware where the heap must never be used at runtime, use xTaskCreateStatic() and xQueueCreateStatic() to eliminate heap allocations entirely.
Detailed Explanation
FreeRTOS does not use the C standard library malloc() and free() directly. Instead, it provides five interchangeable heap implementations — heap_1 through heap_5 — each a separate source file in FreeRTOS/Source/portable/MemMang/. You include exactly one of these files in your build; it provides the pvPortMalloc() and vPortFree() functions that the kernel uses for every dynamic allocation.
For background on when to use FreeRTOS at all, see What Is an RTOS? and Bare-Metal vs RTOS: Which Should You Use for Your Firmware?.
The Five Heap Allocators
| Allocator | Free supported | Coalesces blocks | Memory source | Recommended when |
|---|---|---|---|---|
| heap_1 | No | — | Static ucHeap[] array | Never-free, startup-only allocations |
| heap_2 | Yes | No | Static ucHeap[] array | Avoid — superseded by heap_4 |
| heap_3 | Yes | — | C stdlib malloc/free | External library requires stdlib malloc |
| heap_4 | Yes | Yes | Static ucHeap[] array | Most embedded projects |
| heap_5 | Yes | Yes | Multiple caller-defined regions | Multi-region non-contiguous SRAM |
heap_1 allocates from a static array and never frees — vPortFree() is a no-op. It gives completely deterministic allocation timing, which suits safety-critical systems where every allocation happens at startup and the heap is never touched at runtime. The downside: freed objects waste space permanently.
heap_2 introduced free support but omits coalescing: freed blocks are returned to the pool as-is, without merging adjacent free gaps. Over time, repeated alloc/free cycles with varying sizes fragment the pool into many small gaps that cannot serve larger requests even when total free space is adequate. heap_2 is generally superseded by heap_4 and should be avoided in new designs.
heap_3 wraps the C standard library malloc() and free() with scheduler suspension to make them thread-safe. configTOTAL_HEAP_SIZE is ignored — heap size is determined by the linker script. Use heap_3 only when a third-party library calls malloc() internally and you cannot replace it. For all other purposes, heap_3 introduces a toolchain dependency that FreeRTOS's own allocators avoid.
heap_4 is a coalescing best-fit allocator over a single static ucHeap[] array of configTOTAL_HEAP_SIZE bytes. When vPortFree() is called, heap_4 merges any newly adjacent free blocks before returning them to the pool. This prevents the fragmentation heap_2 accumulates. heap_4 is the standard choice for virtually all non-safety-critical FreeRTOS designs.
heap_5 uses the same coalescing algorithm as heap_4 but accepts multiple memory regions defined by the caller, allowing FreeRTOS to span non-contiguous SRAM banks. configTOTAL_HEAP_SIZE is not used; instead, call vPortDefineHeapRegions() before any pvPortMalloc():
/* STM32F4: SRAM1/2 + CCM, sorted in ascending address order */
HeapRegion_t xHeapRegions[] = {
{ ( uint8_t * ) 0x20000000, 0x20000 }, /* 128 KB SRAM1/2 */
{ ( uint8_t * ) 0x10000000, 0x10000 }, /* 64 KB CCM */
{ NULL, 0 } /* terminator */
};
vPortDefineHeapRegions( xHeapRegions );
The region array must be sorted in ascending address order. vPortDefineHeapRegions() must be called before xTaskCreate(), xQueueCreate(), or any other FreeRTOS call that allocates heap — typically as the first line in main().
pvPortMalloc() and vPortFree()
pvPortMalloc() and vPortFree() are the only allocation functions you should call in FreeRTOS applications. They are thread-safe (protected by suspending the scheduler or entering a critical section, depending on heap implementation) and work consistently across all five allocator implementations.
/* Allocate a 64-byte buffer */
uint8_t *pBuf = pvPortMalloc( 64 );
if ( pBuf == NULL ) {
/* heap exhausted — handle the error */
configASSERT( 0 );
}
/* Free when done */
vPortFree( pBuf );
pBuf = NULL; /* prevent use-after-free */
Every FreeRTOS kernel object allocated dynamically — task TCBs, task stacks, queues, semaphores, mutexes, event groups, software timers — uses pvPortMalloc() internally. Every xTaskCreate(), xQueueCreate(), or xSemaphoreCreateBinary() call draws from the same heap pool your application code uses.
Task heap cost: xTaskCreate() allocates a TCB (typically 96–168 bytes depending on FreeRTOS version and FreeRTOSConfig.h options) and the task's stack (usStackDepth × sizeof(StackType_t) bytes; sizeof(StackType_t) is 4 bytes on ARM Cortex-M). A task with usStackDepth = 256 therefore consumes approximately 1,024 bytes of stack plus 96–168 bytes for the TCB — roughly 1.1–1.2 KB per task. See How Does the Memory Map Work in an Embedded Microcontroller? for how this relates to the MCU's SRAM layout.
IPC object heap cost: queues allocate sizeof(Queue_t) (approximately 88 bytes in recent FreeRTOS versions) plus uxQueueLength × uxItemSize bytes for the message buffer. A 10-item queue carrying 4-byte messages consumes roughly 128 bytes. Semaphores and mutexes are implemented as queues internally; a binary semaphore typically costs approximately 88 bytes. Task notifications, by contrast, are stored in the TCB and consume no additional heap.
Sizing the Heap: configTOTAL_HEAP_SIZE
configTOTAL_HEAP_SIZE (in FreeRTOSConfig.h) defines the size of the ucHeap[] array that heap_1, heap_2, and heap_4 allocate statically at compile time. This array sits in the .bss or .data section, consuming RAM from the linker even before any task runs.
A systematic sizing approach avoids guessing:
- Start generous — set
configTOTAL_HEAP_SIZEto a value that fits in available RAM, e.g. 80% of remaining RAM after code and static variables. - Run a soak test — start all tasks, run for an extended period that exercises the full allocation lifecycle (all tasks created, typical IPC message traffic, any runtime heap allocations).
- Read the watermark — call
xPortGetMinimumEverFreeHeapSize()and calculate peak usage:heap_used = configTOTAL_HEAP_SIZE − xPortGetMinimumEverFreeHeapSize(). - Add margin — set the final
configTOTAL_HEAP_SIZE = heap_used × 1.20(20% headroom), rounded up to the nearest 512-byte boundary.
On an STM32F4 with 192 KB total SRAM, a project with six tasks (256-word stacks each), four queues, and two semaphores typically uses approximately 8–12 KB of heap. Setting configTOTAL_HEAP_SIZE to 16,384 provides comfortable headroom for that level of complexity.
Runtime Monitoring: xPortGetFreeHeapSize() and xPortGetMinimumEverFreeHeapSize()
size_t xFreeNow = xPortGetFreeHeapSize();
size_t xFreeMin = xPortGetMinimumEverFreeHeapSize();
/* Log periodically from a monitoring task */
configPRINTF( ( "Heap: %u free now, %u min ever\r\n",
(unsigned)xFreeNow, (unsigned)xFreeMin ) );
xPortGetFreeHeapSize() returns the total free bytes currently available — a snapshot that can increase or decrease as tasks allocate and free. Do not use it alone for sizing; a brief peak allocation may not be captured at the moment of the call.
xPortGetMinimumEverFreeHeapSize() returns the lowest value xPortGetFreeHeapSize() has ever reached since boot. This is the metric to use for sizing: it captures worst-case peak usage across the full run, not just the current state. Read it during a soak test after all tasks have been running long enough to exercise the full allocation pattern.
Note: xPortGetMinimumEverFreeHeapSize() is not available with heap_1 or heap_2 — it requires heap_4 or heap_5.
Heap Fragmentation
Fragmentation occurs when the heap has enough total free space but no single contiguous block large enough to satisfy an allocation request. pvPortMalloc() returns NULL even though xPortGetFreeHeapSize() shows available bytes.
The failure pattern with heap_2 (no coalescing):
Initial state: [ 256 bytes free ]
alloc A (48): [AAAA][ 208 free ]
alloc B (48): [AAAA][BBBB][ 160 free ]
alloc C (48): [AAAA][BBBB][CCCC][ 112 free ]
free A: [ ][BBBB][CCCC][ 112 free ] ← 48 bytes, not merged
free C: [ ][BBBB][ ][ 112 free ] ← three separate gaps
pvPortMalloc(96) → NULL (largest gap is 112 but BBBB sits between the 48s)
With heap_4, vPortFree() immediately inspects the free list and merges any adjacent free blocks on both sides of the newly freed region. The practical effect is that heap_4 handles the typical embedded allocation pattern — creating RTOS objects at startup, rarely or never freeing them — without accumulating fragmentation.
Fragmentation with heap_4 can still occur if the application repeatedly allocates and frees objects of varying sizes at runtime (atypical in most embedded firmware). If pvPortMalloc() returns NULL despite adequate xPortGetFreeHeapSize() with heap_4 in use, consider allocating all objects at startup and never freeing them, or using fixed-size block pools for runtime-allocated objects.
Heap Overflow Detection
FreeRTOS provides a hook that fires when pvPortMalloc() cannot satisfy a request:
/* FreeRTOSConfig.h */
#define configUSE_MALLOC_FAILED_HOOK 1
/* Implement in application code */
void vApplicationMallocFailedHook( void )
{
/* Halt immediately so the failure is visible */
configASSERT( 0 );
}
Always enable this hook in production firmware. An undetected NULL return from pvPortMalloc() typically manifests later as a hard fault, NULL pointer dereference, or silent data corruption — all far more difficult to trace back to their root cause than an immediate halt at the allocation failure.
For stack overflow detection (separate from heap overflow), enable checking in FreeRTOSConfig.h during development:
#define configCHECK_FOR_STACK_OVERFLOW 2
void vApplicationStackOverflowHook( TaskHandle_t xTask, char *pcTaskName )
{
( void ) xTask;
configASSERT( 0 ); /* pcTaskName identifies the overflowing task */
}
Heap exhaustion and stack overflow together account for the majority of FreeRTOS hard faults during early firmware bringup. See How Do You Debug Embedded Firmware? for a broader bringup diagnostic workflow.
Static Allocation: Eliminating the Heap Entirely
For safety-critical firmware — IEC 61508, ISO 26262, or DO-178C contexts — dynamic heap allocation at runtime is often prohibited because it introduces non-deterministic timing and the risk of runtime allocation failure. FreeRTOS supports fully static allocation:
/* FreeRTOSConfig.h */
#define configSUPPORT_STATIC_ALLOCATION 1
#define configSUPPORT_DYNAMIC_ALLOCATION 0 /* optionally disables pvPortMalloc */
Static task:
static StaticTask_t xTaskBuffer;
static StackType_t xStack[ 256 ]; /* 256 words = 1 KB on ARM Cortex-M */
TaskHandle_t xHandle = xTaskCreateStatic(
vMyTaskFunction,
"MyTask",
256, /* stack depth in words */
NULL, /* task parameter */
2, /* priority */
xStack,
&xTaskBuffer
);
Static queue:
static StaticQueue_t xQueueBuffer;
static uint8_t ucQueueStorage[ 8 * sizeof( MyMsg_t ) ];
QueueHandle_t xQueue = xQueueCreateStatic(
8,
sizeof( MyMsg_t ),
ucQueueStorage,
&xQueueBuffer
);
Static equivalents exist for every kernel object: xSemaphoreCreateBinaryStatic(), xSemaphoreCreateMutexStatic(), xEventGroupCreateStatic(), and xTimerCreateStatic().
When configSUPPORT_STATIC_ALLOCATION == 1, you must provide memory for the idle task (and the timer daemon task, if software timers are enabled), because FreeRTOS cannot allocate these automatically:
void vApplicationGetIdleTaskMemory( StaticTask_t **ppxTCBBuffer,
StackType_t **ppxStackBuffer,
configSTACK_DEPTH_TYPE *puxStackSize )
{
static StaticTask_t xIdleTCB;
static StackType_t xIdleStack[ configMINIMAL_STACK_SIZE ];
*ppxTCBBuffer = &xIdleTCB;
*ppxStackBuffer = xIdleStack;
*puxStackSize = configMINIMAL_STACK_SIZE;
}
Omitting this function with configSUPPORT_STATIC_ALLOCATION set produces an immediate linker error — the kernel references the symbol at link time.
Design Considerations
- Use heap_4 by default. Only deviate with a specific constraint: heap_5 for multi-region SRAM, heap_3 to accommodate an external library's stdlib dependency, heap_1 for never-free startup-only allocation, or full static allocation where no runtime heap use is the design requirement.
- Account for kernel object overhead. Every
xTaskCreate(),xQueueCreate(), and semaphore call draws from the heap. The TCB alone is typically 96–168 bytes; on an STM32F4 with eight FreeRTOS tasks at 256-word stacks each, kernel overhead alone approaches 10–12 KB before a single byte of application heap is allocated. - Measure heap usage; never guess.
xPortGetMinimumEverFreeHeapSize()from a soak test is the only reliable sizing input. CopyingconfigTOTAL_HEAP_SIZEfrom a reference project leads to either wasted RAM or silent heap failures. Zeus Design implements FreeRTOS firmware with heap sizing and profiling built into the development process. - Enable
vApplicationMallocFailedHookunconditionally. A NULL return frompvPortMalloc()without a hook produces a fault that is difficult to diagnose. The hook turns a silent failure into an immediate halt at the point of failure — exactly what you want during bringup and in production. - Never call pvPortMalloc() from an ISR.
pvPortMalloc()suspends the scheduler or enters a critical section during allocation — both have undefined behaviour inside an ISR. Allocate buffers before the ISR fires and pass pointers through a queue.
Common Mistakes
- Not accounting for kernel object heap cost: the most common reason
configTOTAL_HEAP_SIZEis set too small is forgetting that everyxTaskCreate(),xQueueCreate(), and semaphore call draws from the heap in addition to application allocations. Budget task stacks, TCBs, and IPC object storage before sizing the heap for application use. - Mixing stdlib
malloc()withpvPortMalloc(): callingmalloc()directly from FreeRTOS tasks (without heap_3) bypasses FreeRTOS's thread-safety wrapper and corrupts the heap under concurrent access. UsepvPortMalloc()andvPortFree()exclusively. - Using
xPortGetFreeHeapSize()alone for sizing: this is an instantaneous snapshot, not the worst-case high-water mark. A task that allocates briefly and then frees will not appear in this reading if polled at the wrong moment. UsexPortGetMinimumEverFreeHeapSize()during soak testing. - Starving the idle task: if high-priority tasks never block, the idle task never runs — and deleted task memory is never freed. A system where
vTaskDelete()is called regularly but the idle task is starved will exhaust the heap over time even though tasks are being deleted. - Omitting
vApplicationGetIdleTaskMemory()with static allocation: enablingconfigSUPPORT_STATIC_ALLOCATIONwithout providing this function produces a linker error, but the mistake is common enough when first adopting static allocation to be worth highlighting. The timer daemon task has a symmetric callback (vApplicationGetTimerTaskMemory()) if software timers are enabled.
Frequently Asked Questions
- Which FreeRTOS heap allocator should I use?
- Use heap_4 for most embedded projects. It supports free (unlike heap_1), coalesces adjacent free blocks to resist fragmentation (unlike heap_2), does not depend on the C standard library malloc (unlike heap_3), and operates over a single contiguous region (simpler than heap_5). Use heap_5 only when the target MCU has non-contiguous SRAM regions — for example, an STM32F4 with both SRAM1/SRAM2 and CCM — and you need FreeRTOS to use all of them. Use heap_1 only for highly constrained, never-free scenarios where allocation timing must be deterministic. heap_2 is generally superseded by heap_4 and should be avoided for new designs.
- Why does pvPortMalloc return NULL even when xPortGetFreeHeapSize reports plenty of free space?
- This is the fragmentation problem. The heap contains enough total free bytes, but no single contiguous free block is large enough to satisfy the request. It happens most commonly with heap_2, which does not coalesce freed blocks — after many alloc/free cycles, the free space is split into many small gaps that cannot serve larger requests. Switching to heap_4 (which coalesces adjacent free blocks on every vPortFree() call) eliminates this class of failure for most embedded workloads. If fragmentation persists with heap_4, the allocation pattern is pathological (many variable-size objects with overlapping lifetimes) — consider switching to fixed-size pool allocators for those objects.
- What happens to a task's stack and TCB memory when vTaskDelete() is called?
- When a dynamically created task is deleted, its TCB and stack are freed by the idle task — not immediately. The idle task reclaims the memory during its periodic housekeeping. This means the heap space is not available until the idle task runs at least once after the deletion. If the idle task never runs (because all other tasks run at higher priority without ever blocking), deleted task memory is never reclaimed and the heap will eventually exhaust. For statically created tasks (xTaskCreateStatic()), no heap is allocated or freed, and the caller is responsible for the lifetime of the stack array and TCB buffer.
- Can I call malloc() and free() directly in FreeRTOS tasks instead of pvPortMalloc()?
- Strongly discouraged unless using heap_3. pvPortMalloc() is thread-safe because FreeRTOS wraps it with critical sections or scheduler suspension. The C standard library malloc() is not inherently thread-safe — it shares global state across all callers, and calling it concurrently from multiple tasks or from an ISR causes heap corruption. Use pvPortMalloc()/vPortFree() exclusively in FreeRTOS applications. heap_3 exists only to bridge projects that must use an external library calling malloc() internally — it makes stdlib malloc thread-safe via scheduler suspension, but this adds latency and is typically the last resort.
References
Related Questions
How Do You Create and Schedule Tasks in FreeRTOS?
Learn how to create FreeRTOS tasks with xTaskCreate, configure task priorities, size stacks safely, and start the scheduler on ARM Cortex-M MCUs.
How Do FreeRTOS Queues, Semaphores, and Mutexes Work?
How to use FreeRTOS queues, semaphores, and mutexes for inter-task communication — including ISR-safe variants, task notifications, and event groups.
What Is an RTOS (Real-Time Operating System)?
An RTOS is a lightweight operating system that gives embedded firmware deterministic task scheduling. Learn how RTOSes work and when you actually need one.
Bare-Metal vs RTOS: Which Should You Use for Your Firmware?
Bare-metal firmware and RTOS suit different embedded projects. Learn the trade-offs — timing, RAM overhead, complexity — and how to choose.
How Does the Memory Map Work in an Embedded Microcontroller?
The Cortex-M memory map assigns flash, RAM, and peripherals to fixed address regions. Covers STM32 layout, volatile keyword, and how linker scripts map to it.
What Is a Linker Script and What Does It Do?
A linker script controls where firmware code and data land in flash and RAM. Covers MEMORY regions, SECTIONS, LMA/VMA, and the startup symbols it exports.