Summary
We need documentation that helps users avoid common memory leaks when using the React on Rails Pro Node Renderer for server-side rendering.
The Node Renderer reuses V8 VM contexts across requests for performance. This means module-level state in the server bundle persists for the entire lifetime of the worker process (until the worker restarts). Code that works fine in the browser — where each page navigation creates a fresh JS context — can silently leak memory on the server.
This is not a bug in React on Rails or the Node Renderer. The VM context reuse is by design and essential for performance. But users need guidance on what patterns to avoid in their application code.
What the docs should cover
1. Module-level caches without eviction
Any module-level Map, Set, Object, or array used as a cache will grow unboundedly in the Node Renderer because the module is loaded once and reused across all requests.
Example of a leak:
// This cache lives forever in the VM context — entries are never removed
const cache = {};
export function buildUrl(input) {
if (cache[input]) return cache[input];
const result = expensiveComputation(input);
cache[input] = result; // grows with every unique input across all requests
return result;
}
Fix: Either clear the cache between renders, add LRU eviction with a max size, or remove the cache entirely if the computation is cheap.
2. _.memoize and similar unbounded memoization
Lodash's _.memoize uses an unbounded MapCache internally. If used at module scope, it accumulates entries across all SSR requests.
Fix: Use a bounded LRU cache instead, or avoid memoization at module scope for SSR code.
3. Module-level mutable state
Any let variable or mutable ref at module scope persists across requests. While not always a leak (if values are overwritten rather than accumulated), it can cause unexpected behavior.
4. The --max-old-space-size recommendation
Without this V8 flag, Node.js in a container reads the cgroup memory limit and sets a very large heap ceiling. This causes V8 to defer garbage collection, amplifying any existing leaks.
Recommendation: Always set NODE_OPTIONS=--max-old-space-size=<MB> for the Node Renderer, sized appropriately for the container and worker count.
5. Worker restart intervals
Explain how allWorkersRestartInterval and delayBetweenIndividualWorkerRestarts serve as a safety net for memory leaks by periodically killing and restarting workers. Document the tradeoff between restart frequency and memory growth.
Context
This came from investigating OOM crashes in a production app (HiChee). The root cause was an image URL cache at module scope in the application code — a MutableMap that cached every unique image URL transformation and never evicted entries. It had existed for years but only caused OOMs after a React 17→19 upgrade increased the baseline SSR memory footprint enough that the cache growth exceeded the container limit before the worker restart cycle could reclaim it.
The Node Renderer and React on Rails packages themselves have no growing global state — the issue was entirely in application code. But there was no documentation warning users about this pattern.
Summary
We need documentation that helps users avoid common memory leaks when using the React on Rails Pro Node Renderer for server-side rendering.
The Node Renderer reuses V8 VM contexts across requests for performance. This means module-level state in the server bundle persists for the entire lifetime of the worker process (until the worker restarts). Code that works fine in the browser — where each page navigation creates a fresh JS context — can silently leak memory on the server.
This is not a bug in React on Rails or the Node Renderer. The VM context reuse is by design and essential for performance. But users need guidance on what patterns to avoid in their application code.
What the docs should cover
1. Module-level caches without eviction
Any module-level
Map,Set,Object, or array used as a cache will grow unboundedly in the Node Renderer because the module is loaded once and reused across all requests.Example of a leak:
Fix: Either clear the cache between renders, add LRU eviction with a max size, or remove the cache entirely if the computation is cheap.
2.
_.memoizeand similar unbounded memoizationLodash's
_.memoizeuses an unboundedMapCacheinternally. If used at module scope, it accumulates entries across all SSR requests.Fix: Use a bounded LRU cache instead, or avoid memoization at module scope for SSR code.
3. Module-level mutable state
Any
letvariable or mutablerefat module scope persists across requests. While not always a leak (if values are overwritten rather than accumulated), it can cause unexpected behavior.4. The
--max-old-space-sizerecommendationWithout this V8 flag, Node.js in a container reads the cgroup memory limit and sets a very large heap ceiling. This causes V8 to defer garbage collection, amplifying any existing leaks.
Recommendation: Always set
NODE_OPTIONS=--max-old-space-size=<MB>for the Node Renderer, sized appropriately for the container and worker count.5. Worker restart intervals
Explain how
allWorkersRestartIntervalanddelayBetweenIndividualWorkerRestartsserve as a safety net for memory leaks by periodically killing and restarting workers. Document the tradeoff between restart frequency and memory growth.Context
This came from investigating OOM crashes in a production app (HiChee). The root cause was an image URL cache at module scope in the application code — a
MutableMapthat cached every unique image URL transformation and never evicted entries. It had existed for years but only caused OOMs after a React 17→19 upgrade increased the baseline SSR memory footprint enough that the cache growth exceeded the container limit before the worker restart cycle could reclaim it.The Node Renderer and React on Rails packages themselves have no growing global state — the issue was entirely in application code. But there was no documentation warning users about this pattern.