# Storage usage within reasonable limits

- **Pattern:** `ab-001891` (`mobile-offline-storage.storage-security.storage-limits`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001891
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001891)

## Why it matters

An app with no storage quotas or cleanup logic accumulates cache data indefinitely. On devices with limited storage — a real constraint on mid-range and older Android handsets — this causes the OS to terminate the app, deny further writes, or prompt the user to clear data. ISO 25010:2011 performance-efficiency.resource-utilisation requires that apps bound their resource footprint. Beyond the device-health impact, unbounded storage growth retains data that should have been evicted: stale records, orphaned queue items, and superseded cache entries that inflate the attack surface for physical device compromise.

## Severity rationale

Low because storage accumulation degrades device health and UX over time rather than enabling immediate exploitation, but it becomes a reliability defect on constrained devices.

## Remediation

Implement a storage quota check and an LRU or age-based eviction function. Call the eviction function on app launch and after any large batch write.

```tsx
import AsyncStorage from '@react-native-async-storage/async-storage';

const SOFT_LIMIT_BYTES = 50 * 1024 * 1024; // 50 MB

export async function storageUsageBytes(): Promise<number> {
  const keys = await AsyncStorage.getAllKeys();
  const pairs = await AsyncStorage.multiGet(keys);
  return pairs.reduce((sum, [, v]) => sum + (v?.length ?? 0), 0);
}

export async function evictOldCache(maxAgeDays = 7) {
  const cutoff = Date.now() - maxAgeDays * 86_400_000;
  const keys = await AsyncStorage.getAllKeys();
  const cacheKeys = keys.filter(k => k.startsWith('cache_'));
  const stale = cacheKeys.filter(k => {
    const ts = parseInt(k.split('_').pop() ?? '0', 10);
    return ts < cutoff;
  });
  if (stale.length) await AsyncStorage.multiRemove(stale);
}

// On app launch:
const used = await storageUsageBytes();
if (used > SOFT_LIMIT_BYTES) await evictOldCache(3); // tighten window under pressure
```

Apply the same eviction logic to offline queue entries older than 30 days — a queued action that old is almost certainly no longer relevant.

## Detection

- **ID:** `storage-limits`
- **Severity:** `low`
- **What to look for:** Examine storage usage and cleanup. Look for logic that monitors device storage, implements storage quotas, or cleans up old data when storage approaches limits.
- **Pass criteria:** Count all storage cleanup or quota management functions. App has at least 1 storage management mechanism: quota checking, periodic cleanup, or LRU eviction. Storage usage should be bounded to no more than 50MB of local cache. Old data is cleaned up when nearing device storage limits.
- **Fail criteria:** No storage management. App could accumulate unlimited data and fill the device.
- **Skip (N/A) when:** Never — storage management is important for mobile.
- **Detail on fail:** `"No storage management found. App caches data indefinitely and could fill device storage over time."`
- **Remediation:** Implement storage quota management:

  ```tsx
  import AsyncStorage from '@react-native-async-storage/async-storage';

  export async function getStorageUsage(): Promise<{
    used: number;
    limit: number;
  }> {
    try {
      const allKeys = await AsyncStorage.getAllKeys();
      let totalSize = 0;

      const items = await AsyncStorage.multiGet(allKeys);
      items.forEach(([_, value]) => {
        if (value) {
          totalSize += value.length;
        }
      });

      // Estimate limit (iOS: ~100MB, Android varies)
      const LIMIT = 50 * 1024 * 1024; // 50MB soft limit

      return {
        used: totalSize,
        limit: LIMIT,
      };
    } catch (error) {
      console.error('Failed to calculate storage usage:', error);
      return { used: 0, limit: 0 };
    }
  }

  export async function cleanupOldCache(daysToKeep = 7) {
    try {
      const allKeys = await AsyncStorage.getAllKeys();
      const cachePrefix = 'cache_';
      const cutoffTime = Date.now() - daysToKeep * 24 * 60 * 60 * 1000;

      const keysToDelete = allKeys.filter(key => {
        if (!key.startsWith(cachePrefix)) return false;

        const timestamp = parseInt(key.split('_')[1], 10);
        return timestamp < cutoffTime;
      });

      if (keysToDelete.length > 0) {
        await AsyncStorage.multiRemove(keysToDelete);
        console.log(`Cleaned up ${keysToDelete.length} old cache entries`);
      }
    } catch (error) {
      console.error('Failed to cleanup cache:', error);
    }
  }

  // Periodically check storage and cleanup
  setInterval(async () => {
    const usage = await getStorageUsage();
    const usagePercent = usage.used / usage.limit;

    if (usagePercent > 0.8) {
      console.warn('Storage usage above 80%, cleaning up...');
      await cleanupOldCache(3); // Keep only 3 days
    }
  }, 60 * 60 * 1000); // Check hourly
  ```

---

## External references

- iso-25010 performance-efficiency.resource-utilization

Taxons: cost-efficiency

HTML version: https://auditbuffet.com/patterns/ab-001891
