ESP32: RTC memory, the fast persistent storage that survives deep sleep
Use RTC_DATA_ATTR to keep state across deep sleep on the ESP32. Boot counters, last-known sensor values, and the gotcha that it does not survive a power cycle.
The first battery-powered sensor I built woke up every 5 minutes, read the temperature, sent it over MQTT, and went back to sleep. The thing that surprised me: every time it woke up, it had no idea how many times it had woken up before. The boot counter reset to zero on every wake. I was logging the wrong thing for two days before I figured out why.
The ESP32 has a small chunk of memory that survives deep sleep.
It is called RTC memory (because it is in the real-time clock
domain, which stays powered even when the rest of the chip is
asleep). You declare variables in it with the RTC_DATA_ATTR
attribute, and they keep their values across esp_deep_sleep_*
calls. This tutorial is about when to use it, when not to, and
the gotcha that bites every first-timer.
What RTC memory actually is
Inside the ESP32, there are several memory regions:
- DRAM: regular RAM. Wiped on deep sleep, wiped on power cycle. This is where your variables live.
- RTC slow memory: 8 KB on the classic ESP32, 16 KB on the ESP32-S3. Stays powered during deep sleep, wiped on power cycle.
- RTC fast memory: 8 KB on the classic ESP32, used by the ULP coprocessor. You almost never touch this from Arduino code.
- Flash: several MB. Survives everything. Slow to write, wears out after 10,000-100,000 erase cycles per sector.
When you write RTC_DATA_ATTR int counter = 0;, the variable
goes in RTC slow memory. After esp_deep_sleep_start(), the
chip powers down DRAM but keeps RTC memory powered. When it
wakes, the variable still has the value it had when you went
to sleep.
If you cut power entirely (yank the battery, power cycle the board), RTC memory resets. It is not “non-volatile” in the “survives a power cycle” sense. It is “non-volatile across deep sleep.” That distinction matters.
The RTC_DATA_ATTR attribute
The pattern is one keyword:
#include <WiFi.h>
RTC_DATA_ATTR int bootCount = 0;
RTC_DATA_ATTR float lastTemperature = -999.0;
RTC_DATA_ATTR char lastWill[64] = "";
void setup() {
Serial.begin(115200);
delay(1000);
bootCount++;
Serial.print("Boot #");
Serial.println(bootCount);
// ... read sensor, save to lastTemperature, etc. ...
Serial.println("Going to sleep");
esp_deep_sleep_start();
}
void loop() {
// never reached
}
After every esp_deep_sleep_start(), the chip resets. On wake,
bootCount has the previous value plus one. lastTemperature
has the value you wrote before sleep. No setup needed, no
library to include beyond the ESP32 core.
The use cases
Three patterns where RTC memory is the right tool:
Boot counters and uptime tracking. Every deep-sleep wakeup increments the counter. After a year of waking every 5 minutes, you have ~100,000 boots. Useful for “how long has this sensor actually been deployed” diagnostics.
Last-known sensor value. Before going to sleep, write the last sensor reading to RTC memory. On wake, before the new sensor read completes (which takes time), the dashboard or MQTT client can show the previous value as a placeholder. No “no data” gaps in the chart.
State machine across sleep. If the sensor has modes (“calibrating,” “sending,” “idle”), the mode persists across deep sleep. The chip wakes up knowing what it was doing.
Last-will-style messages. If the chip is supposed to publish a “going offline” MQTT message before sleep, but the network is unreliable, write the intended message to RTC memory first. On the next boot, if the message was never published, send it now. This pattern handles the “chip lost power before it could send the goodbye” edge case.
The size limit
The slow RTC memory region is small:
| Chip | Slow RTC size |
|---|---|
| ESP32 (original, WROOM, WROVER) | 8 KB |
| ESP32-S2 | 8 KB |
| ESP32-S3 | 16 KB |
| ESP32-C3 | 8 KB |
8 KB sounds like a lot until you remember it is shared with the UART, the Wi-Fi stack’s deep-sleep state, and a few other things. Realistically, you have 4-6 KB for your own variables.
If you declare a 2 KB RTC buffer, you have used 25% of the region. The ESP-IDF will warn at compile time, then fail at runtime if you go over. Keep your RTC variables small and counted.
For larger persistent state, use NVS (covered next) or flash.
The “value persists across deep sleep but not power loss” gotcha
This is the bug that catches everyone once.
The setup: a sensor on a battery. The chip wakes, reads the sensor, sends the value, goes back to sleep. You write the sensor ID to RTC memory so you can read it back. After debugging, you swap the battery to test low-voltage behavior.
Now the chip powers up. The boot counter is zero. The sensor ID is gone. The RTC memory reset because the battery was disconnected.
If your code assumes the sensor ID is always present in RTC memory, it now reads garbage (or zero) and misbehaves. The fix is to either:
- Validate the RTC values on boot. If they look like default (zero, uninitialized memory pattern), initialize them.
- Use NVS for anything that must survive a power cycle.
The validation pattern looks like:
RTC_DATA_ATTR uint32_t magic = 0;
RTC_DATA_ATTR int bootCount = 0;
void setup() {
if (magic != 0xCAFEBABE) {
// First boot, or RTC memory was wiped
magic = 0xCAFEBABE;
bootCount = 0;
}
bootCount++;
// ...
}
The magic-number check is cheap and catches both “never written” and “wiped by power cycle.” The cost is 4 bytes of RTC memory, which you have plenty of.
When to use RTC memory vs NVS vs flash
Three options, ordered by access speed:
| Storage | Use case | Speed | Persists across | Wear |
|---|---|---|---|---|
| RTC memory | Boot counters, last-known values, mode state | Fastest (nanoseconds) | Deep sleep only | None (it’s RAM) |
| NVS | Wi-Fi credentials, calibration values, settings | Slower (milliseconds) | Deep sleep + power cycle | Wear-leveled (10 years typical) |
| Flash files | Logs, OTA images, large data | Slowest (milliseconds to seconds) | Everything | Per-sector wear (10,000-100,000 cycles) |
For things the chip needs immediately on wake (before Wi-Fi,
before flash), RTC memory is the right pick. Reading NVS takes
a few milliseconds; reading a flash file takes tens of
milliseconds. If you want the boot counter printed in the first
line of setup(), RTC memory is the only option that lets you
do it in zero time.
For things the user changes (Wi-Fi password, MQTT topic, calibration constant), use NVS. The user might power-cycle the device and expect their settings to be there.
For things that accumulate over time (logs, sample buffers), use flash files with a wear-aware write pattern.
When something breaks
- “RTC memory resets on every wake.” You are using
RTC_DATA_ATTRon a regular variable, but the chip is doing a power-on reset, not a deep sleep wake. Check thatesp_deep_sleep_start()is actually being called and that the reset reason isESP_SLEEP_WAKEUP_UNDEFINED(power-on) vsESP_SLEEP_WAKEUP_TIMER(deep sleep wake). AddSerial.println(esp_sleep_get_wakeup_cause());to confirm. - “Compile error:
section.rtc_noinit’ will not fit`.” You declared too much RTC memory. The classic ESP32 has 8 KB and the rest is used by ESP-IDF. Trim your buffers or move large state to NVS. - “Values look like random garbage on the first boot.” RTC memory is not initialized on cold boot. The first read after power-up might be anything. Always check the magic number (or a sensible value range) before using the data.
What to build next
- A deep-sleep sensor that publishes a “wake counter” to MQTT once per day, so you can verify the chip is actually waking and sleeping on schedule without watching the serial monitor.
- A “last-known-value” pattern: on every wake, send the previous reading alongside the new one. The dashboard can fill in gaps with the previous value when the network is flaky.
- The NVS tutorial (
esp32-nvs-storage) for storing things that must survive a power cycle: Wi-Fi credentials, calibration values, the device name. - The deep sleep tutorial (
esp32-deep-sleep) if you have not read it yet. RTC memory is most useful paired with deep sleep, but the two are independent (you can use RTC memory without deep sleep if you want, though the values still reset on power cycle).