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:
Disk Cache - Persistent cache stored in .envark/cache.json
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
Scan Initiated
When you run envark scan or envark analyze, Envark starts the cache validation process.
File Discovery
Envark walks the project directory to discover all relevant files (source code and .env files). const allFiles = walkDirectory ( projectPath , { maxFiles , maxDepth });
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 );
}
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 ;
}
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 };
Cache Hit or Miss
Cache Hit: Returns cached scan results immediately
Cache Miss: Performs full scan and writes new cache
Cache Expiration
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 )
Cache Expires (5 Minutes)
$ 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"
}
formatCacheAge ( 500 ) // "just now"
formatCacheAge ( 5000 ) // "5s ago"
formatCacheAge ( 120000 ) // "2m ago"
formatCacheAge ( 3700000 ) // "1h ago"
Benchmark: Medium Project (500 files)
Scenario Duration Cache Status First scan 2,340ms miss Second scan (immediate) 82ms hit (0s old) After file edit 2,410ms miss (hash changed) After 6 minutes 2,380ms miss (expired)
Speedup
Cache hit = 28x faster than full scan
Best Practices
Add to .gitignore 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:
Files are being modified between scans
System clock is incorrect (affects timestamp comparison)
Cache directory is not writable
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:
File modification time not updated (e.g., using git checkout)
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