Articles / Embedded

  • esp8266
  • arduino
  • flash
  • wear-levelling
  • configuration
  • reliability

How to Make an ESP8266 Config Survive Flash Wear

The dangerous bit of an ESP8266 config save is not the JSON parser. It is the innocent-looking write that happens every time a setting changes. Flash sectors can only be erased a finite number of times, and the device has no battery-backed disk waiting to rescue a half-finished update.

Stop treating flash like a file

Flash is written in pages but erased in sectors. An ESP8266 flash sector is 4 KiB, while an individual write can be much smaller. Rewriting a few bytes can still mean copying, erasing and rewriting the whole sector.

Never save on every loop iteration. A button bounce, a noisy sensor or a web form that sends repeated updates can turn configuration storage into a tiny endurance test. The ESP8266 Arduino documentation makes the same point about EEPROM emulation: EEPROM.commit() reflashes the sector even when only a small amount of data changed. The library documentation explains the behaviour.

There is another catch. A power cut can happen after the erase and before the new record is complete. The result is not merely an old value. It can be a sector containing a mixture of old and new bytes, or no valid record at all.

Pick a storage primitive that matches the job

On Arduino-ESP8266, the usual choices are EEPROM emulation and LittleFS. They solve slightly different problems.

  • EEPROM emulation gives you a small byte-addressable area, but EEPROM.commit() is the operation that actually copies the RAM buffer into flash. It is simple, which is also why it is easy to call too often.
  • LittleFS gives you files and a filesystem. It is a better fit for several records or larger configuration, but it still relies on flash and still needs sensible write frequency. The ESP8266 core filesystem documentation covers its layout and limits.
  • RTC memory is useful for small state that must survive deep sleep, but it is not a general replacement for durable configuration. Treat it as a cache or a boot hint, not as your only copy of important settings.

Those are storage mechanisms, not a durability protocol. A filesystem can help distribute writes, but your application still needs to decide when a change is real, how a torn write is detected and which copy wins.

Write a record, not a mutable object

A robust small configuration store can be a sequence of fixed-size records in one or more sectors. Each record contains a magic value, format version, sequence number, payload length, the configuration payload and a checksum.

struct ConfigRecord {
  uint32_t magic;
  uint16_t version;
  uint16_t length;
  uint32_t sequence;
  Config config;
  uint32_t crc32;
};

The exact layout is yours to choose. Keep it explicit, use fixed-width integer types and include a format version. Do not checksum the checksum field itself. On boot, scan the records, reject anything with the wrong magic, length, version or checksum, then select the valid record with the highest sequence number.

Validity must be decided from the complete record. A record that contains plausible values is not necessarily a record that was completely written.

Make the write order do useful work

Flash programming normally changes bits in one direction until an erase resets them. That means a record should have an unmistakable commit marker, written after the payload and checksum.

  1. Find the next free slot, or choose a slot in the next sector during compaction.
  2. Write the header with the marker clear.
  3. Write the payload and checksum.
  4. Read the bytes back and validate the checksum.
  5. Write the commit marker last.

On the next boot, only a record with the final marker and a matching checksum is considered committed. If power disappears during any earlier step, the incomplete record is ignored and the previous committed record remains available.

This is a small write-ahead log. The name sounds grander than the mechanism. It is really a row of labelled envelopes, where the last envelope is only accepted after it has been sealed and checked.

Use a ring when settings change often

A single sector still wears out if it is erased for every update. A ring spreads erases across several sectors.

Reserve a region containing, for example, eight sectors. Append records until the active sector is full. Then copy the newest valid configuration into an erased sector, mark that new sector active, and erase the oldest sector only after the new copy has been verified.

Never erase the only known-good copy first. The safe order is: write elsewhere, verify, switch the active generation, then reclaim old space.

Each sector can have its own generation number. At boot, scan sector headers and records. Select the newest valid generation, then the newest valid record within it. A power cut between sector creation and old-sector erasure should leave at least one complete generation.

Do less writing before doing clever writing

The cheapest wear-levelling scheme is refusing pointless writes.

  • Keep the current configuration in RAM.
  • Mark it dirty only when a value actually changes.
  • Debounce changes from buttons and sensors.
  • Coalesce several edits into one save after a quiet period.
  • Save immediately for changes that affect safety or connectivity, but make that exception deliberate.

Saving only changed settings is not enough. If the storage layer rewrites the same sector for every change, you still need a policy that limits how often those writes happen.

A web UI is a classic trap. Saving on every keystroke is convenient for the browser and absurd for flash. Save on an explicit Apply action, or wait until the form is complete.

Design the boot path for bad storage

A config store should have a boring recovery policy. If no valid record exists, load safe defaults, start the device in a provisioning mode if needed, and make the failure visible through a status endpoint or serial log.

Corruption should produce a deliberate fallback. Do not silently turn a corrupted WiFi password into an empty password and then spend ten minutes wondering why the device has vanished. The fallback should be intentional and reversible.

Format changes need the same care as power loss. Accept older versions only when you have a tested migration path. Write the migrated result as a new record, verify it, and retain the old format until the new record is committed.

Test the failures you are pretending to handle

Power-loss safety is not demonstrated by a successful boot after one unplug. Add a host-side test for record parsing, CRC failures, sequence wraparound and truncated writes.

Then exercise the real board. Repeatedly update a test configuration, interrupt power at different points in the save, reboot, and check that the result is either the previous config or the new config. A USB relay or programmable power supply is less exciting than discovering the problem in a customer's airing cupboard.

Test interruption, not just successful writes. Also count writes and erases during development. If the design cannot tell you how often it saves, it is difficult to make an honest endurance estimate.

When a file is the better answer

For a handful of infrequently changed settings, a small journal in a reserved flash region is predictable and easy to test. For larger human-edited configuration, LittleFS with atomic replacement and a backup file may be clearer, provided you understand what the filesystem and board support package guarantee during power loss.

The core rule stays the same: change RAM freely, write flash sparingly, validate complete records, and keep an older good copy until its replacement has earned the right to exist.