When Flash Isn’t Just Flash: Real-World Lessons Using FATFS, LittleFS & Other Filesystems in IoT Devices
Choosing a filesystem in embedded firmware looks simple until real devices start writing logs every few seconds, saving snapshots, updating configs, and recovering from unexpected resets. What starts as a neat API turns into a fight with corruption, wear, mount-time failures, memory stalls, directory explosions, and behavior that rarely shows up in simulation. This post breaks down the actual problems we faced with FATFS, LittleFS, and SPIFFS, why they happened, and the fixes and architectural patterns that finally stabilized our systems.
Filesystems behave differently in theory vs. the real world
Each filesystem promises something on paper. FATFS promises compatibility, simplicity, and wide adoption. LittleFS promises power-loss safety, wear-leveling, and a small footprint. SPIFFS promises minimal metadata and flash-friendliness for a handful of files.
But the moment firmware starts doing real work, writing logs every few seconds, saving snapshots, updating configs, storing intermediate buffers, these abstractions collide with sudden power loss, wear patterns, metadata growth, fragmentation, I/O latency spikes, and unrelated bugs that surface as file corruption. The result: subtle, long-tail failures that take weeks or months to diagnose.
FATFS: where it broke and how we fixed it
Power-loss corruption of FAT tables. Even small file writes, combined with unexpected resets, consistently produced broken FAT chains, orphaned clusters, files that looked normal but held corrupted data, and partial writes that silently truncated. Directory entries, FAT tables, and allocation blocks get updated in separate, non-atomic writes, and any reset in between leaves the filesystem half-updated. We fixed this by reducing write frequency through RAM batching, never updating FATFS inside timing-sensitive code paths, introducing write barriers and delayed commits, adding periodic integrity checks, and moving frequently changing data out of FATFS entirely.
Severe fragmentation over time. As files got appended, deleted, or replaced, FATFS fragmented quickly, showing up as multi-second read times, random slowdowns, pipelines missing timing deadlines, and inconsistent load times across devices. We fixed this by preallocating large files to avoid cluster scatter, replacing append-heavy workloads with circular buffers, consolidating many small files into a single large block, and periodically defragmenting by rewriting the dataset. These alone cut read-time variance dramatically.
No wear awareness, accelerated flash aging. FATFS writes frequently to FAT tables, directory structures, and the first few erase blocks, creating hotspots that wore disproportionately. We fixed this by minimizing directory rewrites, rotating storage regions via custom offsets, reducing small file churn, and shifting volatile, frequently updated data to a flash-friendlier filesystem. Flash lifetime extended significantly.
LittleFS: amazing at power loss, surprisingly tricky elsewhere
LittleFS is often marketed as the filesystem that just works under power loss, and it genuinely is, but it has its own real-world pain points.
Metadata map growth. LittleFS stores everything as an object, so thousands of tiny files means thousands of metadata nodes. That slowed directory traversals, spiked mount times, ballooned metadata blocks, and made free-space reports misleading. We fixed it by no longer creating thousands of tiny files, switching to journaling logs and consolidated structured files, introducing daily or hourly rollovers, and adding automatic log compaction. Mount time went from unpredictable to consistent.
Write amplification. A tiny config write could trigger metadata updates, block relocations, and garbage-collection runs, and we measured up to 5x more flash writes than expected. We fixed it by caching frequently accessed state in RAM, batching updates, migrating config formats to compact single-write structures, and reducing rewrite frequency of stable values. Flash wear dropped substantially.
Random latency spikes from garbage collection. LittleFS performs background or opportunistic GC, and under load that meant random multi-millisecond stalls, blocking writes during critical logic, and interrupts missing their windows. These were the hardest to debug because they looked like timing bugs in unrelated code. We fixed it with an absolute rule against filesystem writes in ISRs or time-critical paths, a dedicated filesystem worker thread, buffering everything, and scheduling GC during idle periods. The system became far more deterministic.
SPIFFS: simple and lightweight, until you push it
SPIFFS shines for storing a handful of persistent files. Problems showed up as soon as the filesystem needed structure or file churn increased.
No directory support. We hit limits fast trying to categorize or separate data, since everything lives in a flat namespace, causing naming collisions and scaling pain. We fixed it by emulating directory structure through naming conventions, or migrating to LittleFS where real hierarchy was required.
Slow reads and writes under increasing load. SPIFFS is optimized for simple cases, and mixing large and small files increased read times, slowed writes due to full-block erasures, and made latency inconsistent. We fixed it by moving large or frequently updated content to other filesystems and keeping SPIFFS only for small, rarely updated system files.
Heavy full-block rewrites. SPIFFS tends to rewrite entire blocks for even small updates, accelerating wear significantly. We fixed it by avoiding any append-heavy use case, keeping files static, and adopting an alternative filesystem for dynamic workloads. Over time most SPIFFS usage got migrated out entirely.
Implementation patterns that finally stabilized everything
Hybrid filesystem architecture. No filesystem does everything well. We fixed stability by placing frequently changing data in a flash-friendly filesystem, placing bulk sequential data in a simple filesystem, and avoiding one-size-fits-all storage layouts. This reduced corruption and improved longevity.
Never allow real-time logic to write to flash. At any point, the filesystem can stall. Writes must be buffered, deferred, or batch-processed. This alone eliminated timing jitter, DMA starvation, and ISR instability.
Avoid small file explosions. Tiny files wreck metadata, fragmentation, mount times, and wear. Consolidation was the universal fix.
Validate filesystem state after OTA or crashes. We caught issues early by scanning and rebuilding metadata structures after updates.
Takeaways: filesystems are architecture, not just APIs
Decide based on workload, not marketing tags, since different workflows break different filesystems. Treat flash writes as expensive and unpredictable, and never write synchronously from critical paths. Hybrid layouts solve more than they complicate, since no single filesystem is universally reliable. Flash health monitoring is essential, since wear tells you where the architecture is wrong. And always assume the device will lose power at the worst possible moment, LittleFS shines here, FATFS does not.