Phase 1: Beginner 14 min read read

ESP32 Memory Architecture: Flash, SRAM, PSRAM, and RTC Memory Explained

Understand every memory region on the ESP32 — internal SRAM, IRAM, DRAM, flash partitions, RTC memory, and PSRAM — and learn how to use each type effectively.

Updated June 18, 2026

Memory is the Resource You Will Run Out Of First

Of all the ESP32’s resources — processing cycles, GPIO pins, peripheral channels — memory is the one that limits projects most frequently. A sketch that compiles cleanly and runs perfectly with Wi-Fi disabled may crash or behave erratically when Wi-Fi connects, because the Wi-Fi stack consumes roughly 90 KB of heap at runtime. An HTTP client with TLS encryption adds another 40–60 KB. Add a JSON parser and a sensor data buffer, and you can exhaust the available 300 KB remarkably fast.

Understanding the ESP32’s memory architecture — what types of memory exist, where they are mapped, and which is appropriate for each use case — transforms these crashes from mysterious failures into predictable, solvable engineering problems.

The Five Memory Regions

The ESP32 has five distinct memory regions, each with different characteristics, speeds, and lifetimes. They are not interchangeable.

1. Internal SRAM (520 KB)

The ESP32 has 520 KB of static RAM on the chip itself, divided into three banks at the hardware level. From the software perspective, this SRAM splits into two main categories:

DRAM (Data RAM) — addresses 0x3FFB0000 to 0x3FFFFFFF — stores your global variables, heap allocations, and the task stacks managed by FreeRTOS. This is the memory returned by malloc(), new, and String object creation. The first region (0x3FFB0000–0x3FFB7FFF, 32 KB) is reserved for data used during ROM boot; the rest is available to applications. Typical free DRAM heap in a minimal Arduino sketch with Wi-Fi enabled: 280–330 KB.

IRAM (Instruction RAM) — addresses 0x40070000 to 0x4009FFFF — stores executable code that must reside in RAM rather than being fetched from flash. Time-critical interrupt service routines and functions marked with IRAM_ATTR are placed here. IRAM access is faster than flash cache access because it has no cache latency. The tradeoff is that IRAM is smaller and its capacity is mostly consumed by the Wi-Fi and Bluetooth stacks in the default configuration.

2. External Flash (4–16 MB)

The ESP32 chip itself has no embedded flash. External flash is a separate SPI NOR flash chip connected to the ESP32 via a dedicated quad-SPI interface running at 80 MHz. The flash chip sits inside the WROOM-32 metal shield next to the ESP32 die.

Flash is accessed through a transparent cache system. When the CPU fetches an instruction that is not in the 64 KB instruction cache, the cache controller reads a 32-byte line from flash automatically. From the programmer’s perspective, code stored in flash runs as if it were in RAM — just slightly slower due to occasional cache misses. Read-only data (string literals, lookup tables marked PROGMEM or placed in flash sections) is also cached through a separate 32 KB data cache.

Flash cannot be written at arbitrary addresses like RAM. It must be erased in 4 KB sectors before writing, and each sector supports approximately 100,000 erase cycles before wear causes bit errors. This is why logging sensor data to flash in a tight loop is a bad idea — the ESP32’s flash will wear out within days. Use RTC memory or PSRAM for frequent writes, and write to flash only when data must persist across power cycles.

3. Flash Partition Table

The flash address space is divided into named partitions by a partition table stored at flash offset 0x8000. Each partition has a type (app or data), a subtype (factory firmware, OTA slot, NVS, SPIFFS, etc.), a start address, and a size. The standard partition table for a 4 MB flash device looks like this:

Name Type Offset Size Purpose
nvs data 0x9000 20 KB Key-value persistent storage
phy_init data 0xF000 4 KB PHY calibration data
app0 app 0x10000 1.9 MB Main firmware
app1 app 0x1F0000 1.9 MB OTA update target
spiffs data 0x3D0000 192 KB File system

You can customise this table to suit your project. A project that never uses OTA updates can remove one app partition and dedicate that space to a larger file system. A project that serves large HTML, CSS, and JS files from SPIFFS may use a “huge app” partition scheme that allocates 3 MB to the file system.

4. NVS — Non-Volatile Storage

NVS is a key-value store in flash that uses a wear-levelling algorithm to distribute writes across multiple flash pages, extending the effective write endurance from 100,000 cycles per sector to millions of cycles across the NVS partition. The NVS page-level format handles power loss corruption gracefully — a partial write is detected and rolled back on the next boot.

In Arduino IDE, the Preferences library wraps NVS. You open a namespace (like a database table), then read and write typed values:

Preferences prefs;
prefs.begin("wifi", false);           // namespace "wifi", read/write
prefs.putString("ssid", "MyNetwork");
prefs.putString("pass", "secret123");
prefs.end();

// On next boot:
prefs.begin("wifi", true);            // read-only
String ssid = prefs.getString("ssid", "");
prefs.end();

NVS is the correct storage location for: Wi-Fi credentials, device names, calibration offsets, user configuration settings, and session tokens. Do not store large data blobs in NVS — it is not designed for files. Keep individual values under a few hundred bytes.

5. RTC Memory (8 KB + 8 KB)

RTC (Real-Time Clock) slow memory (8 KB) and RTC fast memory (8 KB) are powered by the ESP32’s RTC domain, which remains active during deep sleep. This is the only RAM that survives a deep sleep cycle — all other SRAM contents are lost when the main power domains shut down.

Declare variables in RTC slow memory with the RTC_DATA_ATTR attribute:

RTC_DATA_ATTR int boot_count = 0;
RTC_DATA_ATTR float last_temperature = 0.0;

void setup() {
  boot_count++;
  Serial.printf("Boot number: %dn", boot_count);
}

On each wake from deep sleep, boot_count retains its previous value. Use RTC memory for: wake-up counters, accumulated sensor readings between uploads, last-known sensor values, and deep sleep timeout tracking. The ULP co-processor’s code and data also reside in RTC slow memory, allowing sensor reads during deep sleep without waking the main cores.

PSRAM — External Pseudo-Static RAM

PSRAM (also called SPI RAM or SPIRAM) is an external RAM chip mounted inside WROVER modules alongside the ESP32 chip and flash. Standard WROVER modules include 4 MB PSRAM; newer S3-based WROVER modules may include 8 MB octal-SPI PSRAM. PSRAM connects via the SPI bus at up to 80 MHz and has access latency roughly 5× higher than internal SRAM — significant but not prohibitive for bulk data buffers.

Enable PSRAM in the Arduino IDE under Tools → PSRAM → Enabled. Allocate to PSRAM explicitly:

uint8_t *image_buf = (uint8_t*) ps_malloc(320 * 240 * 2);  // 150 KB frame buffer
if (!image_buf) {
  Serial.println("PSRAM allocation failed");
}

Without PSRAM, a 320×240 RGB565 frame buffer (150 KB) would consume half the available internal SRAM heap, leaving almost nothing for Wi-Fi, the web server, and other buffers. With PSRAM, the frame buffer lives externally and internal SRAM remains available for latency-sensitive operations.

Memory Monitoring in Practice

Add these calls to your setup() to baseline memory usage before deep sleep, OTA updates, or large operations:

Serial.printf("Free heap:      %d bytesn", ESP.getFreeHeap());
Serial.printf("Min free heap:  %d bytesn", ESP.getMinFreeHeap());
Serial.printf("Max alloc:      %d bytesn", ESP.getMaxAllocHeap());
if (psramFound()) {
  Serial.printf("Free PSRAM:   %d bytesn", ESP.getFreePsram());
}

Watch getMinFreeHeap() — it shows the lowest heap level reached since boot, revealing peak usage spikes that a snapshot getFreeHeap() call might miss. If getMinFreeHeap() drops below 20 KB, you are at risk of allocation failures.

Avoiding Memory Problems

Prefer fixed-size global arrays over dynamic heap allocations for buffers whose maximum size is known. Avoid the Arduino String class for repeated concatenation — it fragments the heap with repeated alloc/free cycles; use char[] arrays and snprintf() instead. For JSON, use ArduinoJSON’s JsonDocument with a size calculated by the library’s ArduinoJSON Assistant tool before allocating. For HTTP responses, stream them in chunks rather than buffering the entire body before processing. These practices extend the effective RAM budget of any ESP32 project without requiring more hardware.

Frequently Asked Questions

Projects to Build

Put this knowledge to work — try one of these hands-on projects.