Lodash Memoize: Preventing Cache Memory Leaks

By default, the Lodash _.memoize function provides no built-in automatic eviction mechanisms—such as Time-to-Live (TTL) expiration or Least Recently Used (LRU) clearing—to prevent memory leaks. Because its internal cache retains arguments and return values indefinitely, developers must rely on built-in extension points to manage memory. This article explains how the Lodash caching system operates and outlines the specific mechanisms available to prevent unbounded memory growth, including manual cache invalidation, custom cache constructors, and WeakMap implementations.

The Default Cache Behavior

When a function is wrapped with _.memoize, Lodash creates an internal cache store using _.memoize.Cache, which defaults to JavaScript’s native Map (falling back to a polyfilled object map in older environments). Every unique computed key adds an entry to this map.

Because standard Map instances hold strong references to both keys and values, the garbage collector cannot reclaim cached data, even if the application no longer requires it. Without intervention, long-running processes or high-cardinality inputs will cause the memory footprint to expand continually.

Manual Cache Invalidation

Lodash explicitly attaches the underlying storage instance to the memoized function via the cache property. This mechanism allows developers to inspect, modify, or completely clear stored entries manually:

While manual invalidation allows programmatic control, it requires external triggers (such as timers, events, or lifecycle hooks) to manage memory cleanup actively.

Custom Cache Constructors

The primary architectural mechanism Lodash provides to avert memory leaks is the ability to override the default cache constructor. The _.memoize.Cache property can be replaced globally, or a custom instance can be attached to a specific memoized function.

To implement bounded caching, developers can substitute an LRU cache or a fixed-size cache library that adheres to the Map interface (implementing has, get, set, and delete):

const LRU = require('lru-cache');

// Configure a bounded cache implementation
_.memoize.Cache = class {
  constructor() {
    this.cache = new LRU({ max: 500 });
  }
  has(key) { return this.cache.has(key); }
  get(key) { return this.cache.get(key); }
  set(key, val) { this.cache.set(key, val); return this; }
  delete(key) { return this.cache.delete(key); }
  clear() { this.cache.clear(); }
};

Using a bounded cache limits the maximum number of items in memory, automatically evicting older entries when new ones arrive.

WeakMap for Object Keys

If the memoized function exclusively accepts objects as cache keys, the cache constructor can be replaced with native WeakMap. A WeakMap maintains "weak" references to its keys, meaning that if an object key has no other references remaining elsewhere in the application, the garbage collector will automatically collect both the key and the cached value. This approach entirely eliminates memory leaks for object-based lookups without requiring manual cache management.

Key Resolution Control

By default, Lodash only uses the first argument passed to the memoized function as the cache key. Lodash allows a custom resolver function as a second parameter to _.memoize. A poorly written resolver can generate an excessive number of unique string keys, accelerating memory exhaustion. By defining strict, controlled resolver functions, developers can limit the creation of redundant entries and keep the overall cache footprint predictable.