Skip to main content
UltraInstinct
Back to latest articles
Consumer Technology•By ••11 min read

How Android ZRAM Writeback Works on Modern Smartphones

Understand Android ZRAM writeback mechanics, from zsmalloc cold page eviction and backing store block allocation to UFS flash write wear and swap latency.

Featured visual representing How Android ZRAM Writeback Works on Modern Smartphones

Consumer smartphones operate under strict physical DRAM limits, typically packaging between 8 GB and 16 GB on flagship mobile platforms. Client application footprints grow with every release cycle. Background services, multi-tab web engines, camera processing buffers, and local machine learning models compete for physical frames. Operating systems manage this pressure through Linux swap backed by zram, compressing anonymous pages with algorithms like LZ4 or Zstandard and storing them directly in memory managed by zsmalloc. Compression delivers space savings around 2.5:1 to 3:1. When total memory allocation continues to climb, compressed buffers crowd out file-backed page caches and trigger aggressive process termination through the low-memory killer daemon (lmkd).

To prevent premature task eviction, platform vendors deploy Android ZRAM writeback. This kernel subsystem finds idle, uncompressed, or poorly compressing pages inside the compressed RAM block device and flushes them to secondary flash storage. Instead of allowing incompressible 4 KB memory chunks to waste physical memory, the kernel evicts them out to an asynchronous backing storage file or raw flash partition on the universal flash storage (UFS) array.

Transferring memory pages from RAM to flash fundamentally alters client system performance. Swap operations shift from memory copies completing in 2 to 5 microseconds to asynchronous block I/O operations taking 100 to 500 microseconds. Understanding how the kernel tracks page access frequency, manages backing device sector allocators, and controls flash wear determines whether a device maintains smooth frame times or stalls under swap thrashing.

Memory Pressure and the Limits of In-RAM Compression

Linux swap on mobile platforms differs from desktop storage swap. Smartphone flash historically lacked the endurance and random I/O throughput required to survive continuous page swaps. The kernel creates a virtual block device /dev/block/zram0 backed by physical memory allocations. When the kernel memory management subsystem enters direct reclaim or triggers kswapd, it selects anonymous pages from active and inactive list structures, pushes them into zram0, and frees original struct page physical frames back to the buddy allocator.

The memory manager inside zram relies on zsmalloc to allocate variable-sized compressed objects. Standard slab allocators suffer heavy internal fragmentation when packing compressed byte streams of non-uniform sizes, such as 1,340 bytes or 2,110 bytes. The zsmalloc allocator groups physical pages into composite structures called zspage clusters to store compressed objects within defined size classes. Objects map to specific physical spans, yielding low internal fragmentation under standard workloads.

System stability degrades when applications allocate non-compressible data. Encrypted buffers, JPEG bitstreams, zipped audio assets, and pre-compressed video frames do not compress. When zram attempts to compress these payloads, the output size often matches or exceeds original 4 KB page sizes. The kernel identifies these pages as huge objects. Storing a 4,096-byte payload along with zsmalloc metadata inside RAM provides zero memory savings while adding CPU overhead during compression and decompression cycles.

The problem escalates on devices running non-standard page granularities. As covered in Why Android 16-KB Page Sizes Break Native Mobile Apps, moving from 4 KB to 16 KB base page allocations multiplies allocation overhead inside zsmalloc size classes. Incompressible 16 KB pages occupy entire system frames without yielding headroom. When physical memory fills with idle pages and incompressible slabs, kswapd cycles continuously. The CPU consumes power attempting to compress unshrinkable pages, direct reclaim stalls foreground touch threads, and lmkd kills background processes to recover space.

How Android ZRAM Writeback Identifies and Marks Idle Pages

Android ZRAM writeback resolves memory saturation by establishing a multi-tiered eviction ladder. The driver creates a secondary storage path using the zram_writeback kernel interface. Cold pages and incompressible chunks evacuate RAM entirely, landing on persistent storage.

Tracking cold pages relies on page access state bits. The kernel exposes an administrative knob through sysfs at /sys/block/zram0/idle. Writing timestamps or mode flags to this file instructs zram to traverse its internal table of entries. The driver inspects the flags field in each struct zram_table_entry:

struct zram_table_entry {
    unsigned long handle;
    unsigned long flags;
    spinlock_t lock;
};

During evaluation passes, the driver checks whether an entry has experienced read or write operations since the last sweep. If the entry remained untouched, the kernel assigns the ZRAM_IDLE bit to the entry flags field:

/* Marking entries idle in drivers/block/zram/zram_drv.c */
static void mark_idle(struct zram *zram, ktime_t cutoff)
{
    down_write(&zram->init_lock);
    for (index = 0; index < zram->disksize >> PAGE_SHIFT; index++) {
        struct zram_table_entry *entry = &zram->table[index];

        if (!zram_allocated(entry))
            continue;

        if (entry->ac_time < cutoff)
            entry->flags |= ZRAM_IDLE;
    }
    up_write(&zram->init_lock);
}

The system issues writeback directives by writing specific mode strings to /sys/block/zram0/writeback. Four primary modes govern target selection:

  1. idle: Instructs the subsystem to scan for entries marked with ZRAM_IDLE. Any compressed or uncompressed page unreferenced since the previous marking pass gets scheduled for disk eviction.
  2. huge: Targets pages where compressed length plus metadata equals or exceeds full physical frame size. These pages waste RAM space and move to disk regardless of access age.
  3. huge_idle: Combines both filters. It selects entries that compress poorly and have remained unreferenced, prioritizing the most inefficient RAM allocations first.
  4. all: Directs the driver to flush all candidate entries to disk, typically used during automated memory health sweeps prior to system suspension.

Once tagged, candidate entries are locked. The driver allocates a storage block on the backing storage device, reads the compressed payload out of zsmalloc, and issues asynchronous write requests to the underlying block layer.

Block Allocation and Swapping to Backing Flash Storage

The backing store for zram operates via a configured loopback file or dedicated raw block partition. During system initialization, Android platform scripts set up the backing block device:

# Associate backing block device with zram instance
echo "/dev/block/by-name/zram_wb" > /sys/block/zram0/backing_dev

When /sys/block/zram0/backing_dev receives a valid block device path, the kernel opens the block device with blkdev_get_by_dev(), verifies geometry, and initializes an entry tracking bitmap. Each bit in this allocation bitmap maps to one 4 KB sector on the backing storage device.

The driver executes page eviction through zram_writeback(). The execution sequence processes targets sequentially:

+-------------------------------------------------------------+
| 1. Scan zram_table_entry for target flag (IDLE / HUGE)      |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
| 2. Allocate free slot index from backing device bitmap      |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
| 3. Decompress payload from zsmalloc into scratch page buffer|
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
| 4. Construct bio, set bi_iter.bi_sector to allocated slot   |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
| 5. submit_bio() asynchronous write to physical UFS storage  |
+-------------------------------------------------------------+
                              |
                              v
+-------------------------------------------------------------+
| 6. Set ZRAM_WB in entry->flags, free zsmalloc memory block  |
+-------------------------------------------------------------+

When evicting a page, zram allocates an unused sector offset from its internal bitmap. If the target page sits compressed inside zsmalloc, the kernel temporarily decompresses the bytes into an intermediate scratch buffer because flash devices require aligned 4 KB block structures. It populates a struct bio, assigns REQ_OP_WRITE flags, binds the target backing sector address, and hands the request to the block layer via submit_bio().

Upon write completion, the driver records the allocated sector index inside entry->element, sets the ZRAM_WB bit flag, and clears the memory address pointer. The driver calls zs_free(zram->mem_pool, handle) to release compressed memory frames back to the OS. The page now occupies zero physical DRAM.

/* Updating table entry state following successful writeback */
entry->element = blk_idx;
entry->flags |= ZRAM_WB;
entry->flags &= ~ZRAM_IDLE;
zram_free_page(zram, index);

When an application thread wakes up and accesses a written-back memory frame, a normal page fault occurs. The Linux virtual memory subsystem routes the access to zram0. The zram_bvec_read() routine checks the flags field. Finding ZRAM_WB set, the driver allocates an I/O read bio, calculates the backing flash sector address from entry->element, and submits a synchronous REQ_OP_READ block request.

This synchronous read incurs substantial latency. A standard in-memory zram decompression operation returns control to the CPU in 2 to 5 microseconds. An on-flash random read across the UFS interface requires 120 to 250 microseconds under uncongested conditions. If the read queue conflicts with background storage operations, latency spikes to several milliseconds. During heavy asset initialization, this discrepancy leads to visible interface stutters unless mitigated by page cache lookaheads, such as those detailed in How Page Fault Prefaulting Speeds Up Mobile Game Loading.

Flash Endurance Costs and Controller Buffer Depletion

Writing memory pages to client flash introduces physical silicon trade-offs. Mobile devices use Triple-Level Cell (TLC) or Quad-Level Cell (QLC) NAND flash architectures. TLC cells store 3 bits of charge per cell and survive between 1,000 and 3,000 program/erase (P/E) cycles. QLC cells endure roughly 500 to 1,000 P/E cycles.

Directing unbounded swap write traffic to flash degrades NAND endurance rapidly. Write Amplification Factor (WAF) worsens the wear profile. Flash memory cannot overwrite existing data in place; it writes across 4 KB to 16 KB pages but erases data in large block blocks spanning 4 MB to 16 MB. Random 4 KB writes forced by page writeback operations prevent efficient garbage collection routines, raising internal WAF ratios above 3.0.

To maintain burst write performance for consumer workloads, modern storage implements high-speed pseudo-SLC caching zones, as explained in How UFS 4.0 WriteBooster Works in Consumer Flash Storage. The UFS host writes incoming blocks into Single-Level Cell (SLC) buffers at speeds exceeding 1,500 MB/s. However, continuous memory writeback routines deplete available SLC cache pools. When the WriteBooster buffer exhausts its allocated capacity, the UFS controller drops into synchronous folding mode:

[In-Memory ZRAM Pool]
          |
   (Writeback Burst)
          v
[UFS Controller SLC Buffer (WriteBooster)] ---> [Buffer Exhausted]
                                                        |
                                                 (Direct Folding)
                                                        v
                                            [TLC/QLC Physical Blocks]
                                            (Throughput drops 70%)
                                            (Latency spikes to 20ms)

During direct folding, the controller reads data out of its SLC blocks, recalculates error correction codes (ECC), and writes 3-bit or 4-bit states into dense physical TLC blocks while simultaneously handling new incoming host write streams. Device write throughput plummets from 2,800 MB/s down to 300 MB/s, and read latency escalates from 150 microseconds to 20 milliseconds. If the foreground UI thread faults on a written-back page while the flash controller executes folding, the entire user interface freezes.

Android mitigates flash wear and controller exhaustion through write limits managed by platform daemons. The kernel driver enforces a daily quota mechanism through /sys/block/zram0/writeback_limit and /sys/block/zram0/writeback_limit_enable. When enabled, the driver tracks every written sector. If total writes hit the defined threshold—typically 4 GB to 8 GB per 24-hour window—the driver rejects subsequent writeback attempts with -EIO or -ENOSPC, forcing lmkd to terminate background apps rather than degrade device longevity.

Configuring and Tracing Writeback Operations in Production

Configuring Android ZRAM writeback requires configuring block interfaces within platform init.rc configuration files during startup. The following setup commands configure a 4 GB RAM swap disk backed by a 2 GB dedicated storage partition:

# Configure backing storage target
losetup /dev/block/loop10 /data/per_boot/zram_swap
echo /dev/block/loop10 > /sys/block/zram0/backing_dev

# Set total disk size for zram (RAM pool + backing capacity)
echo 4294967296 > /sys/block/zram0/disksize

# Initialize daily writeback limit to 4 GB (1048576 4KB pages)
echo 1048576 > /sys/block/zram0/writeback_limit
echo 1 > /sys/block/zram0/writeback_limit_enable

# Set swap priority and activate block device
mkswap /dev/block/zram0
swapon -d /dev/block/zram0 -p 32767

User-space system management services (such as Android's perfetto or custom OEM health monitors) sweep idle pages periodically. The runtime marks pages idle when the screen turns off, then schedules a background writeback pass:

# Mark all currently stored pages as idle
echo all > /sys/block/zram0/idle

# Flush incompressible huge pages and unreferenced idle pages to flash
echo huge_idle > /sys/block/zram0/writeback

Performance engineers monitor writeback health and backing store utilization by inspecting /sys/block/zram0/bd_stat. This interface outputs block counts detailing physical I/O behavior:

cat /sys/block/zram0/bd_stat
# Output columns:
# [pages_read] [pages_written] [pages_failed]

To extract real-time metrics programmatically, engineers read bd_stat alongside general mm_stat structures:

#define _GNU_SOURCE
#include 
#include 
#include 
#include 

void inspect_zram_health(void) {
    FILE *bd = fopen("/sys/block/zram0/bd_stat", "r");
    FILE *mm = fopen("/sys/block/zram0/mm_stat", "r");
    
    if (!bd || !mm) {
        perror("Failed to access zram sysfs nodes");
        return;
    }

    unsigned long bd_reads = 0, bd_writes = 0, bd_fails = 0;
    if (fscanf(bd, "%lu %lu %lu", &bd_reads, &bd_writes, &bd_fails) != 3) {
        perror("Malformed bd_stat");
    }

    unsigned long orig_size = 0, comp_size = 0, mem_used = 0;
    if (fscanf(mm, "%lu %lu %lu", &orig_size, &comp_size, &mem_used) >= 3) {
        printf("ZRAM In-Memory: %lu MB -> %lu MB\n", orig_size >> 20, comp_size >> 20);
    }

    printf("Backing Store IO: Read=%lu pages, Write=%lu pages, Failures=%lu\n",
           bd_reads, bd_writes, bd_fails);

    fclose(bd);
    fclose(mm);
}

int main(void) {
    inspect_zram_health();
    return 0;
}
ponytail: basic stats parser; upgrade with inotify watcher when tracking live spikes.

To diagnose latency spikes caused by faulting pages from flash, engineers trace block events using the kernel ftrace infrastructure. Monitoring entry into the swap fault path isolates delays to the exact microsecond:

# Configure ftrace for zram and block layer events
echo 0 > /sys/kernel/tracing/tracing_on
echo "zram:*" >> /sys/kernel/tracing/set_event
echo "block:block_bio_queue" >> /sys/kernel/tracing/set_event
echo "block:block_bio_complete" >> /sys/kernel/tracing/set_event
echo 1 > /sys/kernel/tracing/tracing_on

# Perform workload evaluation, then inspect output trace
cat /sys/kernel/tracing/trace | grep -E "(zram_wb|block_bio)"

Trace outputs reveal the latency distribution between internal decompression and physical storage fetch operations. If block_bio_complete timestamps span beyond 1,000 microseconds during user interactions, engineers adjust the idle cutoff threshold, throttle writeback volume, or configure lmkd to evict cached background runtimes before writeback fills the backing store.

Conclusion

Android ZRAM writeback alters how consumer mobile operating systems manage physical memory exhaustion. Evicting idle, uncompressible memory frames from RAM to flash storage relieves pressure on zsmalloc memory pools and allows background applications to remain cached longer. This operational model trades fast RAM swap speeds for increased I/O latency and puts stress on physical NAND endurance limits.

Maintaining a responsive client device requires managing these operational boundaries. Incompressible huge pages provide an immediate optimization target for flash eviction, but uncontrolled writeback bursts risk saturating UFS SLC WriteBooster buffers and forcing the storage controller into heavy garbage collection cycles. Setting daily writeback quotas, tuning idle age thresholds, and monitoring storage fault latency ensures platforms extract maximum multi-tasking headroom without degrading physical flash storage or stalling user interface responsiveness.

References

  1. Linux Kernel Organization. (2024). zram: Compressed RAM based block device documentation. https://www.kernel.org/doc/html/latest/admin-guide/blockdev/zram.html
  2. Android Open Source Project. (2024). Android Performance and Low Memory Management. https://source.android.com/docs/core/perf/boot-times
  3. Torvalds, L. et al. (2024). Linux Kernel Tree: drivers/block/zram/zram_drv.c. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/block/zram/zram_drv.c