50% OFF

ESP32-IDF Workshop

Blog/Embedded Systems

ESP32 Memory Architecture: IRAM, DRAM, Stack, and Heap Explained

Stack overflows are the #1 silent killer of ESP32 production firmware. Understanding IRAM, DRAM, stack and heap allocation is the foundation of stable firmware.

| Intermediate
Rajath Kumar
Rajath KumarEdge AI Engineer & Founder, Analog Data
2026-06-23·10 min read
ESP32 Memory Architecture: IRAM, DRAM, Stack, and Heap Explained

Stack overflows don't announce themselves. Your firmware runs fine for hours, maybe days — then at 3am it reboots. The logs show a corrupt stack trace pointing nowhere useful. You added a local variable somewhere. That was the last straw.

This is Part 2 of the ESP32 Production Firmware series. Understanding memory is the foundation of everything else — task pinning, IPC patterns, and OTA updates all depend on getting memory right first.


ESP32 Memory Map

text
1┌──────────────────────────────────────┐  0x4007_FFFF
2│           IRAM (128 KB)              │  ← Code that needs zero-latency execution
3│  ISRs, time-critical functions       │
4├──────────────────────────────────────┤  0x4000_0000
5│                                      │
6│           Flash (up to 4 MB)         │  ← Your compiled code lives here
7│   .text section (most functions)     │     (cached, ~5ns extra latency)
8│                                      │
9├──────────────────────────────────────┤
10│           DRAM (520 KB)              │
11│                                      │
12│  ┌───────────────────────────────┐   │
13│  │  .bss / .data (globals)       │   │  ← Static/global variables
14│  ├───────────────────────────────┤   │
15│  │  FreeRTOS task stacks         │   │  ← One per task, fixed at creation
16│  ├───────────────────────────────┤   │
17│  │  WiFi/BT stack (~150 KB)      │   │  ← Reserved by ESP-IDF automatically
18│  ├───────────────────────────────┤   │
19│  │  Heap (malloc/new)            │   │  ← Dynamic allocations
20│  └───────────────────────────────┘   │
21└──────────────────────────────────────┘

The WiFi stack alone consumes ~150 KB of your DRAM. This is non-negotiable — it's reserved the moment you call esp_wifi_init(). Plan your remaining 350 KB accordingly.


IRAM vs Flash: When It Matters

Most of your code lives in Flash and is fetched through the instruction cache. This works perfectly well for normal functions. But for interrupt service routines (ISRs) and functions called at very high frequency, cache misses introduce unpredictable latency.

c
1// ❌ Normal function — lives in Flash, may have cache miss in time-critical context
2void gpio_isr_handler(void *arg) {
3    // If this code isn't in cache, it fetches from Flash — 15-30ns penalty
4    gpio_set_level(OUTPUT_PIN, 1);
5}
6
7// ✅ IRAM function — always in fast on-chip RAM, zero cache miss
8void IRAM_ATTR gpio_isr_handler(void *arg) {
9    // Always executes at full speed, no cache dependency
10    gpio_set_level(OUTPUT_PIN, 1);
11}

Rules for IRAM_ATTR:

  • All ISR handlers
  • Functions called from ISR context
  • Functions called at > 10,000 Hz in tight loops
  • Time-critical sensor polling functions

IRAM is only 128 KB — use it sparingly. Don't mark entire modules with it.


Task Stack: The Silent Killer

Every FreeRTOS task gets a dedicated stack, allocated at task creation time and fixed for the life of the task:

c
1xTaskCreate(
2    my_task,
3    "my_task",
4    4096,    // ← stack size in BYTES — not kilobytes, not words
5    NULL,
6    5,
7    &handle
8);

This 4096 bytes covers:

  • All local variables in my_task and every function it calls
  • Function call return addresses
  • Saved CPU register state during context switches
  • Any temporary buffers allocated on the stack

What eats stack silently

c
1void my_task(void *pvParameters) {
2    char json_buffer[512];        // ← 512 bytes gone, every iteration
3    char log_message[256];        // ← 256 more bytes
4    SensorReading readings[10];   // ← depends on struct size, maybe 200 bytes
5
6    while (1) {
7        // At this point, you've used ~1KB before doing anything
8        format_json(json_buffer, readings);  // that function has its own locals too
9        vTaskDelay(pdMS_TO_TICKS(1000));
10    }
11}

With a 4096-byte stack and 1KB already consumed by locals, your actual working headroom for function calls is only ~3KB. Call one function that does printf-style formatting and you've burned another 512 bytes.


Measuring Stack Usage in Production

Never guess. Measure:

c
1void stack_monitor_task(void *pvParameters) {
2    extern TaskHandle_t sensor_task_handle;
3    extern TaskHandle_t mqtt_task_handle;
4    extern TaskHandle_t inference_task_handle;
5
6    while (1) {
7        // uxTaskGetStackHighWaterMark returns minimum free bytes EVER recorded
8        // Lower value = closer to overflow you've been
9        ESP_LOGI("MEM", "sensor    stack: %4lu bytes free",
10            uxTaskGetStackHighWaterMark(sensor_task_handle));
11        ESP_LOGI("MEM", "mqtt      stack: %4lu bytes free",
12            uxTaskGetStackHighWaterMark(mqtt_task_handle));
13        ESP_LOGI("MEM", "inference stack: %4lu bytes free",
14            uxTaskGetStackHighWaterMark(inference_task_handle));
15
16        // Also monitor heap
17        ESP_LOGI("MEM", "heap free: %lu bytes (min ever: %lu)",
18            esp_get_free_heap_size(),
19            esp_get_minimum_free_heap_size());
20
21        vTaskDelay(pdMS_TO_TICKS(10000));
22    }
23}

Run this in development under full production load — all sensors active, WiFi connected, MQTT publishing, display updating. The minimum values you see are your headroom.


Stack Sizing Rules

ScenarioRecommended Stack
Simple periodic task (no printf, no buffers)2048 bytes
Task with logging and small buffers4096 bytes
Task using sprintf, JSON formatting6144–8192 bytes
MQTT task with TLS8192–12288 bytes
Edge AI inference (TFLite Micro)16384–32768 bytes
OTA update task8192 bytes minimum

Never ship firmware without running stack watermark logging for at least 24 hours under full load. A stack overflow that you never triggered in testing will find you at the worst possible time in production.


Heap vs Stack: When to Use Each

c
1// Stack allocation — fast, automatic cleanup, limited size
2void process_sensor_data(void) {
3    float readings[20];        // fine — small, short-lived
4    char msg[64];              // fine — small, short-lived
5    // ... automatically freed when function returns
6}
7
8// Heap allocation — flexible size, manual management, fragmentation risk
9void init_large_buffer(void) {
10    uint8_t *dma_buffer = (uint8_t *)heap_caps_malloc(4096, MALLOC_CAP_DMA);
11    if (!dma_buffer) {
12        ESP_LOGE(TAG, "Failed to allocate DMA buffer!");
13        return;
14    }
15    // ... use buffer ...
16    free(dma_buffer);   // must free or you leak memory permanently
17}

Heap allocation rules for production:

  1. Always check the return value — malloc can return NULL
  2. Free what you allocate — heap fragmentation is real and fatal
  3. Use heap_caps_malloc for DMA-capable or IRAM-resident buffers
  4. Monitor esp_get_minimum_free_heap_size() — if it keeps shrinking, you have a leak

Key Takeaways

  1. WiFi stack eats ~150 KB — plan your DRAM budget with this as the fixed cost
  2. IRAM_ATTR for ISRs — everything interrupt-critical must be in IRAM
  3. Stack is fixed at task creation — size it correctly upfront, measure it in development
  4. uxTaskGetStackHighWaterMark is your best friend — run it for 24 hours under load
  5. Heap fragmentation is permanent — free everything, check every malloc return value

What's Next

In Part 3, we'll cover dual-core task pinning — how to deliberately assign tasks to Core 0 or Core 1, why it matters for WiFi stability, and the exact allocation patterns used in production IoT gateways.

Share
Live Workshop

Go from Arduino to Production Firmware

The ESP32-IDF Workshop covers ESP-IDF from scratch — tasks, queues, OTA, Wifi management, and deploying firmware that doesn't break at 3am.

Join the Workshop →

Frequently Asked Questions

Quick answers to common questions

Rajath Kumar

Written by

Rajath Kumar

Edge AI Engineer & Founder, Analog Data

I build things that run on chips and the software that talks to them. ESP32, STM32, FreeRTOS, FastAPI, TinyML — from bare-metal firmware to cloud backends to on-device inference. Based in Bengaluru. Founder of Analog Data.

More in Embedded Systems