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.
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
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.
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:
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_taskand 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
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:
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
| Scenario | Recommended Stack |
|---|---|
| Simple periodic task (no printf, no buffers) | 2048 bytes |
| Task with logging and small buffers | 4096 bytes |
Task using sprintf, JSON formatting | 6144–8192 bytes |
| MQTT task with TLS | 8192–12288 bytes |
| Edge AI inference (TFLite Micro) | 16384–32768 bytes |
| OTA update task | 8192 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
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:
- Always check the return value —
malloccan returnNULL - Free what you allocate — heap fragmentation is real and fatal
- Use
heap_caps_mallocfor DMA-capable or IRAM-resident buffers - Monitor
esp_get_minimum_free_heap_size()— if it keeps shrinking, you have a leak
Key Takeaways
- WiFi stack eats ~150 KB — plan your DRAM budget with this as the fixed cost
- IRAM_ATTR for ISRs — everything interrupt-critical must be in IRAM
- Stack is fixed at task creation — size it correctly upfront, measure it in development
uxTaskGetStackHighWaterMarkis your best friend — run it for 24 hours under load- Heap fragmentation is permanent — free everything, check every
mallocreturn 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.
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.
Frequently Asked Questions
Quick answers to common questions

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.