< Back to all hacks

#17 Compile Cache Cleanup

RAM
Problem
Node 22 compile cache accumulates 51 MB of duplicate .cache files.
Solution
Periodic rm -rf of node compile cache directory. Rebuilds at 27 MB, saves 24 MB.
Lesson
Compile caches grow monotonically and never self-evict. On constrained devices, schedule periodic cleanup to reclaim disk space.

Context

Node.js 22 introduced module.enableCompileCache() and the NODE_COMPILE_CACHE environment variable, which persist compiled V8 bytecode to disk for faster module loading on subsequent startups. The gateway sets NODE_COMPILE_CACHE=$PREFIX/tmp/v8-cache to take advantage of this.

In practice, the compile cache on the Moto E2 accumulates files over time but provides minimal benefit. The gateway rarely restarts (uptimes of days or weeks are normal), so the cache is almost never read back. Meanwhile, the cache directory grows to 51 MB as V8 stores a bytecode file for every module that gets loaded, including lazily-loaded modules that may be loaded only once.

On a phone with roughly 3 GB of usable storage (after Android, Termux, and the OpenClaw installation), 51 MB is significant. Cleaning the cache and letting it rebuild on the next restart drops it to 27 MB, saving 24 MB.

There is also an ironic twist on this specific device: module.enableCompileCache() returns status 2 (ENABLED) but the actual cache files are often 0 bytes. V8's compile cache has known issues on ARM32, and the bytecode is never actually serialized to disk. The directory fills with thousands of empty or near-empty .cache files, wasting inodes and directory metadata space rather than actual byte storage. The cleanup is still worthwhile for filesystem overhead.

Implementation

Create the cleanup script:

#!/data/data/com.termux/files/usr/bin/bash
# $PREFIX/bin/clean-compile-cache.sh
# Clean Node.js V8 compile cache to reclaim disk space

CACHE_DIR="/data/data/com.termux/files/usr/tmp/v8-cache"

# Check size before cleanup
BEFORE=$(du -sh "$CACHE_DIR" 2>/dev/null | cut -f1)
COUNT=$(ls -1 "$CACHE_DIR" 2>/dev/null | wc -l)

# Remove and recreate the directory
rm -rf "$CACHE_DIR"
mkdir -p "$CACHE_DIR"

# Log the cleanup
echo "$(date '+%Y-%m-%d %H:%M') cache cleaned: $BEFORE ($COUNT files)" \
  >> /data/data/com.termux/files/home/.openclaw-cache-log

Make it executable:

chmod +x $PREFIX/bin/clean-compile-cache.sh

Add the hourly cron job using busybox crontab:

# busybox crontab is at $PREFIX/bin/applets/crontab
$PREFIX/bin/applets/crontab -l > /tmp/current-cron 2>/dev/null || true
echo "0 * * * * $PREFIX/bin/clean-compile-cache.sh" >> /tmp/current-cron
$PREFIX/bin/applets/crontab /tmp/current-cron
rm /tmp/current-cron

Verify the cron entry:

$PREFIX/bin/applets/crontab -l
# Should show: 0 * * * * /data/data/com.termux/files/usr/bin/clean-compile-cache.sh

Ensure crond is running (started automatically by the boot script, but verify manually):

pgrep -f crond || $PREFIX/bin/applets/crond -b -L /dev/null

The NODE_COMPILE_CACHE environment variable is set in the gateway start script:

# In $PREFIX/bin/start-openclaw
export NODE_COMPILE_CACHE="/data/data/com.termux/files/usr/tmp/v8-cache"

For reference, the compile cache can also be enabled programmatically in hijack.js:

// In hijack.js (loaded via -r flag before OpenClaw)
const mod = require('module');
if (typeof mod.enableCompileCache === 'function') {
  const result = mod.enableCompileCache();
  // result.status: 0 = FAILED, 1 = ALREADY_ENABLED, 2 = ENABLED
  // On ARM32: returns 2 but files are 0 bytes
}

Verification

  • du -sh $PREFIX/tmp/v8-cache shows current cache size before cleanup.
  • Run the cleanup script: $PREFIX/bin/clean-compile-cache.sh
  • du -sh $PREFIX/tmp/v8-cache shows 4.0K (empty directory) after cleanup.
  • Restart the gateway and check: du -sh $PREFIX/tmp/v8-cache rebuilds to approximately 27 MB.
  • ls $PREFIX/tmp/v8-cache/ | wc -l shows the number of cache files (typically 200-400).
  • cat ~/.openclaw-cache-log shows cleanup history with sizes and file counts.
  • $PREFIX/bin/applets/crontab -l shows the scheduled cleanup job.
  • After 24 hours, the log should show multiple hourly cleanup entries.

Gotchas

  • Cleaning the cache while the gateway is running is safe. Node.js opens cache files at module load time and does not hold file descriptors open afterward. The running process already has compiled bytecode in V8's in-memory code cache.
  • The mkdir -p after rm -rf is essential. If the directory does not exist when the gateway next starts, Node.js silently disables compile caching rather than creating the directory itself. The NODE_COMPILE_CACHE path must already exist before the Node process starts.
  • On ARM32, compile cache files are often 0 bytes due to V8 serialization limitations on 32-bit platforms. The disk savings from cleanup come primarily from directory entry overhead, inode allocation, and filesystem journal entries rather than actual file content bytes.
  • The hourly schedule is a balance. More frequent cleanup (every 15 minutes) wastes CPU cycles for minimal additional savings. Less frequent cleanup (daily) lets the cache grow back to 51 MB between cleanups. Hourly keeps it under 30 MB.
  • Do not use find ... -mtime +1 -delete for selective age-based eviction. All cache files are created at gateway startup time, so they all have the same mtime. A full wipe-and-recreate is simpler and equally effective.
  • If crond is killed by the Android low memory killer, the cleanup job stops running silently. The boot script (~/.termux/boot/start-pocketclaw.sh) restarts crond on boot, but an OOM kill between boots leaves no cleanup running. Check with pgrep -f crond if disk usage grows unexpectedly.
  • The cleanup script logs to ~/.openclaw-cache-log. This log itself grows unboundedly. Truncate it occasionally: tail -100 ~/.openclaw-cache-log > /tmp/cl && mv /tmp/cl ~/.openclaw-cache-log.

Result

The compile cache directory stays under 30 MB instead of growing unboundedly past 51 MB. On a storage-constrained phone, reclaiming 24 MB of disk space per cycle helps maintain headroom for logs, temporary files, and npm operations. The cleanup runs silently via cron with no impact on the running gateway, and the cache rebuilds automatically on the next restart.