Memory Allocation Strategies: Static, Heap Caps, and Fragmentation
At Hoomanely, our embedded platforms power an ecosystem of modular IoT products where reliability, uptime, and predictability matter more than raw throughput. Our System-on-Module architecture spans sensing, communication, and processing nodes, all running continuously under diverse workloads, so memory allocation isn't a theoretical concern, it defines real-world reliability across devices deployed in customer homes.
Memory allocation is one of the most fundamental yet misunderstood parts of embedded systems design. A single poorly timed malloc call, or an unpredictable burst of dynamic allocations, can destabilize an otherwise well-architected device. On constrained systems without swap, memory is finite and fragile, and even when total free memory looks sufficient, fragmentation can prevent the allocator from satisfying a request for lack of a large enough contiguous block. This post covers three strategies that keep firmware stable over months of continuous uptime: static allocation, heap caps, and fragmentation control.
Unbounded allocation and fragmentation
Dynamic allocation carries two major risks. Allocation failure happens when temporary peak loads exhaust available heap, when long-lived objects create holes between freed blocks, or when external fragmentation blocks large allocations despite plenty of total free memory. Long-term fragmentation builds slowly as short-lived and long-lived allocations intermix on the same heap, buffer sizes vary wildly, subsystems behave differently under different workloads, and memory entropy climbs monotonically. A device expected to run for months or years can't tolerate this drift, a device that passes a 24-hour stress test can still fail after 60 days from accumulated fragmentation.
Strategy one: static allocation
Static allocation reserves memory at compile time and skips the runtime allocator entirely, with objects placed in .data or .bss at link time.
#define SENSOR_BUF_SIZE 512
static uint8_t sensor_buffer[SENSOR_BUF_SIZE];It's essential where it fits because it gives zero fragmentation (the layout never changes), zero allocation overhead (no runtime calls, no metadata bookkeeping), fully deterministic timing, and it's ideal for ISRs, real-time control loops, and critical sections. The trade-off is inflexibility, it can't support variable-sized structures, may waste RAM when conservatively sized for the worst case, and requires accurate worst-case sizing up front. Static allocation gives determinism but not flexibility, it's the foundation everything else builds on.
Strategy two: heap caps and memory pooling
Dynamic allocation is necessary in real systems, but it has to be controlled.
Heap caps set a maximum memory budget per module or subsystem, giving isolation and preventing any one component from monopolizing resources. A heap cap wraps the standard allocator with accounting, tracking usage against a ceiling, returning NULL if the cap would be exceeded, and protecting operations with a mutex for thread safety. This gives memory isolation between subsystems, predictable worst-case usage known at design time, protection from one module starving another, and easier debugging since leaks stay contained within a clear boundary. Hoomanely devices run sensing loops, communication threads, and utility tasks concurrently, and heap caps keep them isolated, for example capping the network stack at 16KB, sensor processing at 8KB, and utilities sharing 4KB.
Memory pools provide pre-sized blocks for frequent allocation patterns, eliminating fragmentation and allocation overhead at once. A pool is an array of fixed-size blocks with a free list or bitmap tracking availability, giving O(1) allocation and deallocation, no fragmentation since fixed-size blocks never create holes, cache-friendly sequential layout, and a natural fit for protocol packets, queue nodes, and structs. Pools work best for network packet buffers with a known MTU, message queue nodes, sensor data structures with fixed schemas, and state machine context objects, anywhere allocation sizes cluster around known values like 64, 128, or 256 bytes.
Strategy three: fragmentation management
Even with caps and pools, fragmentation can still emerge when dynamic allocation spans multiple components or handles variable-sized data.
External fragmentation happens when free memory exists but not contiguously, from mixing long-lived and short-lived objects on the same heap, highly variable buffer sizes, frequent allocate-and-free cycles creating "swiss cheese" memory, or bursty workloads leaving permanent holes at peak usage. Internal fragmentation comes from allocator rounding, per-block metadata overhead, alignment requirements, and minimum allocation sizes.
Mitigation techniques that work well: arena, or linear, allocators bump a pointer forward and free everything at once, eliminating fragmentation for temporary workloads like request-response cycles, JSON or CBOR parsing, temporary compute buffers, or per-connection state in network servers. Avoiding allocations in ISRs matters just as much, since allocator search time is non-deterministic, heap mutexes can cause priority inversion, many allocators aren't reentrant, and RTOS scheduling guarantees get violated otherwise, the fix is pre-allocated buffers or lock-free ring buffers for ISR-to-task communication. Grouping objects by lifetime, initialization objects allocated once at startup, session objects living for connection duration, request objects allocated and freed per request, keeps long-lived objects from fragmenting regions used by short-lived ones. And preferring fixed-size data structures, ring buffers, fixed-capacity arrays, pre-allocated pools, over dynamically resized ones eliminates both memory exhaustion and fragmentation from variable-length linked structures.
Mapping strategy to Hoomanely's workloads
Our SoM architecture runs sensor loops, communication tasks (MQTT, CoAP, HTTP), and monitoring utilities simultaneously over long durations, with devices expected to hold months of continuous uptime in customer deployments. To keep memory behavior predictable across that heterogeneous workload, critical paths use static allocation, sensor buffers, ISR contexts, real-time state machines, and configuration structures. Frequent patterns use pools, network packets, queue nodes, small messages, event structures. Modules get isolated via heap caps, network stack at 16KB, application layer at 12KB, utilities at 4KB. And batch workloads use arenas, JSON parsing, temporary computations, request handling, protocol encoding and decoding.
A production-ready allocator often combines these based on allocation characteristics:
void* allocate_buffer(size_t sz) {
if (sz <= 128) return pool128_alloc();
void* p = capped_malloc(sz);
if (p) return p;
return arena_alloc(&temp_arena, sz);
}Typical sizes get served by pools for O(1) allocation with zero fragmentation, larger requests stay bounded by the capped heap, and temporary workloads get isolated in an arena so they never contribute to long-term fragmentation.
Monitoring and observability
Effective memory management needs runtime visibility. Production systems should track current usage, peak usage as a high-water mark over the device's lifetime, failed allocation counts, the largest free block as an indicator of external fragmentation, and a fragmentation ratio, one minus the largest free block over total free memory. Useful debug techniques include periodic heap walks to detect corruption, allocation tracking that records caller addresses to find leak sources, watermark checking with sentinel values around buffers to catch overruns, and periodic snapshots comparing heap state over time to catch slow leaks.
Key takeaways
Static allocation gives deterministic execution with zero runtime overhead and zero fragmentation. Heap caps isolate memory budgets and stop subsystems from interfering with each other through hard limits. Pools eliminate fragmentation for common patterns with O(1) fixed-size allocation. Arenas simplify temporary workloads by allowing bulk deallocation instead of individual frees. Avoid allocations inside interrupts to preserve real-time guarantees and prevent priority inversion. Group objects with similar lifetimes together to reduce fragmentation through heap segregation. Monitor fragmentation metrics to catch degradation before failure. And use hybrid strategies that combine multiple techniques for robust operation.
Predictability is what long-running embedded devices like Hoomanely's IoT modules need. Memory allocation strategy isn't an optimization detail, it's a fundamental architectural decision that determines whether a device runs reliably for hours or years.