Why Node.js vm Triggers OOM in V8: Hidden Compilation Cache Pitfalls

This article explains how frequent use of Node.js's vm module can cause out‑of‑memory crashes in V8 due to an uncollected compilation cache, the role of the --always‑promote‑young‑mc flag, and practical work‑arounds for Node.js 12, 14 and 16.

Node Underground
Node Underground
Node Underground
Why Node.js vm Triggers OOM in V8: Hidden Compilation Cache Pitfalls

Talk is Cheap, Show Me the Code

First, a minimal reproducible script is presented to illustrate the problem.

Note: In an asynchronous while loop, manually call gc() at the end of each iteration.

Reproducing OOM

Running the script with Node.js 12 shows memory rising to about 100 MB and then dropping repeatedly, which is normal.

With Node.js 14 and 16 the same script crashes with an OOM error because V8’s bug (or feature) is triggered.

Note: Experiments were done on Node.js 14.16.0 and 16.8.0; later versions may fix the issue.

V8 Compilation Cache

Cache Technique

The V8 Compilation Cache is a low‑level hash table shared by the whole V8 isolate, using the source string as the key. It differs from the cachedData field of the vm API.

When the vm module compiles a script, it creates an UnboundScript object that looks up the cache using the source string. If a cache entry exists, it is returned; otherwise the script is compiled, the result is stored in the cache, and the source string becomes the cache key.

In real‑world pages this cache can achieve about 80 % hit rate and is fast because it lives in memory.

Cache GC Mechanism

The cache is only reclaimed by the CollectAllAvailableGarbage() GC pass, not by the regular CollectAllGarbage() pass. When the heap reaches its limit, V8 performs a “Last Resort” GC, which invokes CollectAllAvailableGarbage() to try to free the cache.

If the allocation still fails after this GC, the process crashes with a fatal error.

Temporary Fix

Disabling the compilation cache (e.g., with --no-compilation-cache) prevents the memory growth.

Old‑generation Allocation Failure Logic

V8’s allocation routine tries to allocate memory; on failure it runs a couple of normal GCs, then a “Last Resort” GC ( CollectAllAvailableGarbage()). If allocation still fails, an OOM error is thrown.

--always‑promote‑young‑mc Flag

In V8 v8.0.1 a new flag --always-promote-young-mc was introduced. When enabled, every Full GC promotes all young objects to the old generation, forcing an old‑generation allocation. If that allocation fails, V8 aborts with “MarkCompactCollector: young object promotion failed”, which matches the crash observed on Node.js 14/16.

"last resort gc" means that there was an allocation failure that a normal GC could not resolve.

--array‑buffer‑extension Flag Interaction

The --array-buffer-extension flag (read‑only, set at compile time) forces --always-promote-young-mc to be enabled. Node.js 14.5.0 turned this flag on, making it impossible to disable the promotion flag.

On versions prior to 14.5.0 the flag was off, allowing users to run Node.js with --no-always-promote-young-mc to avoid the OOM.

Summary

The vm module relies on UnboundScript, which uses V8’s Compilation Cache that is only cleared by CollectAllAvailableGarbage(), causing memory to keep growing.

Node.js 14/16’s V8 triggers an OOM after the heap limit is reached due to the promotion flag.

The cache can be disabled with --no-compilation-cache, but the promotion flag cannot be turned off because it is forced by --array-buffer-extension.

Solutions

Track the V8 bug/feature and follow its resolution.

Avoid frequent creation of vm contexts in performance‑critical code.

Easter Egg

When debugging the process with the Inspector, GC behavior changes and the OOM no longer occurs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Node.jsgarbage collectionMemory Leakv8OOMCompilation Cache
Node Underground
Written by

Node Underground

No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.