Skip to main content
Envark includes a sophisticated caching system that dramatically improves scan performance on repeated runs. This page explains how the cache works, when it’s used, and how to manage it.

Overview

Envark uses a two-tier caching strategy:
  1. Disk Cache - Persistent cache stored in .envark/cache.json
  2. Memory Cache - Session-based in-memory cache for the current process
On a typical project with 500+ files, caching can reduce scan time from 2-3 seconds to under 100ms.

Disk Cache

Location

The disk cache is stored in your project directory:
my-project/
├── .envark/
   └── cache.json      # Scan results cache
├── .env
├── src/
└── package.json
Add .envark/ to your .gitignore to avoid committing cache files.

Cache Structure

The cache file contains:
interface CacheEntry<T> {
    hash: string;           // Content hash of all scanned files
    timestamp: number;      // When the cache was created (Unix ms)
    expiresAt: number;      // When the cache expires (Unix ms)
    data: T;                // Cached scan results
}
Example cache.json:
{
  "hash": "a3f8c9d7e2b1",
  "timestamp": 1709654400000,
  "expiresAt": 1709654700000,
  "data": {
    "usages": [
      {
        "variableName": "DATABASE_URL",
        "filePath": "/path/to/src/db.ts",
        "lineNumber": 12,
        "language": "typescript",
        "usageType": "usage"
      }
    ],
    "scannedFiles": 342,
    "sourceFiles": 287,
    "envFiles": 3
  }
}

How Cache Validation Works

1

Scan Initiated

When you run envark scan or envark analyze, Envark starts the cache validation process.
2

File Discovery

Envark walks the project directory to discover all relevant files (source code and .env files).
const allFiles = walkDirectory(projectPath, { maxFiles, maxDepth });
3

Hash Computation

A hash is computed from all discovered files’ metadata:
function computeFilesHash(files: FileInfo[]): string {
    // Hash combines:
    // - File paths
    // - File sizes
    // - Modification times
    // - File count
    
    const data = files
        .map(f => `${f.relativePath}:${f.size}:${f.modifiedTime}`)
        .sort()
        .join('|');
    
    return createHash('md5').update(data).digest('hex').slice(0, 12);
}
4

Cache Lookup

Envark attempts to read the cache file:
const cached = readCache(projectPath, hash);
if (cached.hit && cached.data) {
    // Use cached results
    return cached.data;
}
5

Validation Checks

The cache is considered valid only if:✅ Cache file exists at .envark/cache.json
✅ Hash matches current file state
✅ Cache has not expired (< 5 minutes old)
// Check hash match
if (entry.hash !== hash) {
    return { hit: false, data: null };
}

// Check expiry
if (Date.now() > entry.expiresAt) {
    return { hit: false, data: null };
}

return { hit: true, data: entry.data };
6

Cache Hit or Miss

Cache Hit: Returns cached scan results immediately
Cache Miss: Performs full scan and writes new cache

Cache Expiration

CACHE_EXPIRY_MS
number
default:"300000"
Cache automatically expires after 5 minutes (300,000 milliseconds).
const CACHE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes

const entry: CacheEntry<T> = {
    hash,
    timestamp: Date.now(),
    expiresAt: Date.now() + CACHE_EXPIRY_MS,
    data
};

Why 5 Minutes?

The 5-minute expiry balances:
  • Performance - Most development workflows benefit from caching during active editing
  • Freshness - Prevents stale results from being used too long
  • CI/CD - In CI, the cache is rarely hit (different environments), so expiry doesn’t matter

Cache Invalidation

The cache is automatically invalidated when:
Any modification to tracked files invalidates the cache:
# Before
$ envark scan
 Scanned 342 files in 1.8s (cache miss)

# Edit a file
$ echo "const x = process.env.NEW_VAR" >> src/app.ts

# After - hash changes, cache invalidated
$ envark scan  
 Scanned 342 files in 1.9s (cache miss)
The hash includes:
  • File path
  • File size
  • Modification timestamp
So even trivial changes invalidate the cache.
Adding or removing files changes the hash:
# Add a new file
$ touch src/new-feature.ts

# Hash changes because file count changed
$ envark scan
 Scanned 343 files in 1.9s (cache miss)
$ envark scan
 Scanned 342 files in 1.8s (cache miss)

# Immediately run again - cache hit
$ envark scan
 Scanned 342 files in 0.08s (cache hit, 2s old)

# Wait 6 minutes...
$ envark scan
 Scanned 342 files in 1.9s (cache miss - expired)
You can manually clear the cache:
# Delete cache file
$ rm .envark/cache.json

# Or use the CLI command (if available)
$ envark cache clear

Memory Cache

For the current process, Envark also maintains an in-memory cache:
class MemoryCache {
    private cache: Map<string, { data: unknown; timestamp: number }>;
    private ttlMs: number = 30000; // 30 seconds
    
    get<T>(key: string): T | null {
        const entry = this.cache.get(key);
        if (!entry) return null;
        
        if (Date.now() - entry.timestamp > this.ttlMs) {
            this.cache.delete(key);
            return null;
        }
        
        return entry.data as T;
    }
    
    set<T>(key: string, data: T): void {
        this.cache.set(key, { data, timestamp: Date.now() });
    }
}

// Singleton instance
export const sessionCache = new MemoryCache();

Use Cases

  • API Server Mode - If Envark is used as a long-running service
  • Watch Mode - For potential future watch/daemon features
  • Testing - Speeds up test suites that scan the same project repeatedly
The memory cache has a shorter TTL (30 seconds) than the disk cache (5 minutes).

Controlling Cache Behavior

Disable Cache

You can disable caching with the --no-cache flag:
# Force fresh scan, ignore cache
$ envark scan --no-cache
Programmatically:
import { scanProject } from 'envark';

const result = scanProject('/path/to/project', {
    useCache: false  // Disable cache
});

Cache Location

The cache is always stored in the project directory:
const CACHE_DIR = '.envark';
const CACHE_FILE = 'cache.json';

function getCachePath(projectPath: string): string {
    return join(projectPath, CACHE_DIR, CACHE_FILE);
}
There is no global cache directory. Each project has its own isolated cache.

Cache Statistics

You can inspect cache status:
import { getCacheStats, formatCacheAge } from 'envark';

const stats = getCacheStats('/path/to/project');

console.log(stats);
// {
//   exists: true,
//   size: 45673,           // bytes
//   age: 120000,           // milliseconds
//   expired: false
// }

if (stats.age) {
    console.log(formatCacheAge(stats.age));
    // "2m ago"
}

Age Formatting

formatCacheAge(500)        // "just now"
formatCacheAge(5000)       // "5s ago"
formatCacheAge(120000)     // "2m ago"
formatCacheAge(3700000)    // "1h ago"

Performance Impact

Benchmark: Medium Project (500 files)

ScenarioDurationCache Status
First scan2,340msmiss
Second scan (immediate)82mshit (0s old)
After file edit2,410msmiss (hash changed)
After 6 minutes2,380msmiss (expired)

Speedup

Cache hit = 28x faster than full scan

Best Practices

Add to .gitignore

# .gitignore
.envark/
Never commit cache files to version control.

CI/CD: Disable Cache

# .github/workflows/envark.yml
- run: envark analyze --no-cache
In CI, always use fresh scans for accurate results.

Development: Use Cache

Default behavior is perfect for development:
$ envark scan  # Fast repeat scans

Clear Stale Cache

If you suspect cache issues:
$ rm -rf .envark/
$ envark scan

Troubleshooting

Symptoms: Every scan shows “cache miss”Possible Causes:
  1. Files are being modified between scans
  2. System clock is incorrect (affects timestamp comparison)
  3. Cache directory is not writable
  4. Using --no-cache flag
Debug:
# Check if cache exists
$ ls -la .envark/cache.json

# Check cache content
$ cat .envark/cache.json | jq '.timestamp, .expiresAt'

# Check current time
$ date +%s000  # Unix timestamp in milliseconds
Symptoms: Scan results don’t reflect recent code changesPossible Causes:
  1. File modification time not updated (e.g., using git checkout)
  2. Clock skew
Solution:
# Force cache clear
$ rm -rf .envark/
$ envark scan --no-cache
Symptoms: EACCES errors when writing cacheSolution:
# Make directory writable
$ chmod 755 .envark/

# Or disable caching
$ envark scan --no-cache
Caching is optional - Envark will continue working if cache writes fail.

Implementation Details

Cache Write Process

export function writeCache<T>(projectPath: string, hash: string, data: T): boolean {
    try {
        // Ensure .envark/ directory exists
        ensureCacheDir(projectPath);
        
        const cachePath = getCachePath(projectPath);
        
        const entry: CacheEntry<T> = {
            hash,
            timestamp: Date.now(),
            expiresAt: Date.now() + CACHE_EXPIRY_MS,
            data
        };
        
        // Write atomically with pretty formatting
        writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
        return true;
    } catch {
        // Cache writes are best-effort, don't throw
        return false;
    }
}

Cache Read Process

export function readCache<T>(projectPath: string, hash: string): CacheResult<T> {
    const cachePath = getCachePath(projectPath);
    
    if (!existsSync(cachePath)) {
        return { hit: false, data: null };
    }
    
    try {
        const content = readFileSync(cachePath, 'utf-8');
        const entry: CacheEntry<T> = JSON.parse(content);
        
        // Validate hash
        if (entry.hash !== hash) {
            return { hit: false, data: null };
        }
        
        // Validate expiry
        if (Date.now() > entry.expiresAt) {
            return { hit: false, data: null };
        }
        
        const age = Date.now() - entry.timestamp;
        return { hit: true, data: entry.data, age };
    } catch {
        return { hit: false, data: null };
    }
}

Next Steps

Performance

Learn more optimization techniques for large codebases

CI/CD Integration

Set up Envark in your CI/CD pipeline

Build docs developers (and LLMs) love