Skip to content

Docs: add guide for avoiding memory leaks in Node Renderer SSR #2844

@AbanoubGhadban

Description

@AbanoubGhadban

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions