Fixing Eleventy Out-of-Memory Errors on Large Collections

The error arrives without warning on a site that built fine yesterday:

<--- Last few GCs --->
[2841:0x5d3a1c0]   148213 ms: Mark-Compact 4041.2 (4134.6) -> 4027.9 (4136.3) MB, 2210.4 / 0.0 ms
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

On large Eleventy sites this usually happens when a collection crosses some size, or when someone adds a feature that reads every page's rendered content — a related-posts list, a search index, a full-content RSS feed. Eleventy keeps everything it knows about every page in memory for the whole build, so anything that multiplies per-page data by the number of pages grows quadratically in the worst case.

This guide fixes the error on a 12,400-page knowledge base built with Eleventy 3.0 on GitHub Actions' standard 7 GB runner. It shows how to confirm what is holding memory, the four changes that took peak heap from over 4 GB to 1.6 GB, and how to size the Node heap afterwards. It belongs to Astro vs Eleventy for Documentation Sites, where memory behaviour at scale is one of the selection criteria.

Symptoms and First Checks

The build failed after about 2.5 minutes, consistently, with the default Node 20 heap limit of about 4 GB on a 64-bit runner. Before changing anything, three measurements set the baseline:

CheckCommandResult
Peak heap with a raised limitNODE_OPTIONS=--max-old-space-size=8192 /usr/bin/time -v npx @11ty/eleventy5.3 GB resident, build 3 min 41 s
Pages and collection sizesnpx @11ty/eleventy --dryrun with a log in eleventy.after12,400 pages, articles 11,900 items
Global data sizedu -sh _data186 MB of JSON

The 186 MB of JSON was the first surprise: a product catalogue exported nightly, loaded as global data, and available to every template.

Retained heap by category in a snapshot taken at 90 percent of the build Horizontal bars of retained memory. Rendered templateContent strings held by collections 1.9 gigabytes. Global data from the product catalogue JSON 1.4 gigabytes. Eleventy's own page and data cascade objects 0.9 gigabytes. Search index array built in a template 0.7 gigabytes. Other 0.4 gigabytes. Where the heap went templateContent in collections 1.9 GB Catalogue global data 1.4 GB Eleventy page objects 0.9 GB Search index array 0.7 GB Other 0.4 GB
Heap snapshot via node --inspect-brk and Chrome DevTools, grouped by retainer; total about 5.3 GB.

Step 1: Find the Retainers With a Heap Snapshot

Run the build under the inspector with a raised limit so it completes, and take a snapshot near the end:

NODE_OPTIONS=--max-old-space-size=8192 node --inspect-brk node_modules/@11ty/eleventy/cmd.cjs

Open chrome://inspect, continue execution, and take a heap snapshot from the Memory panel when the console shows writing has started. Sort by retained size. Two patterns stood out: thousands of large strings retained by templateContent on collection items, and one huge object retained by the data cascade under catalogue.

The templateContent strings came from three templates — a related-articles block, the full-content RSS feed and a search index template — each of which looped over collections.articles and read item.templateContent. Reading templateContent forces Eleventy to render and retain each item's output, and three templates doing so kept every article's HTML alive for the whole build.

Step 2: Stop Reading templateContent Across Collections

Each of the three templates needed far less than the full HTML:

  • Related articles needed titles, URLs and tags. They are in item.data; the loop stopped touching templateContent at all.
  • RSS needed full content for the 20 newest items only. The feed template now slices first, then reads content: collections.articles | reverse | head(20).
  • The search index needed plain text, truncated to 2,000 characters. It moved out of a template and into an eleventy.after event that reads the written HTML files from disk one at a time, so nothing is held in memory longer than one file.
// eleventy.config.js
eleventyConfig.on('eleventy.after', async ({ results }) => {
  const out = fs.createWriteStream('_site/search.json');
  out.write('[');
  results.forEach((r, i) => {
    if (!r.url.startsWith('/kb/')) return;
    const text = stripHtml(r.content).slice(0, 2000);
    out.write((i ? ',' : '') + JSON.stringify({ url: r.url, text }));
  });
  out.end(']');
});

results contains each page's output once; streaming it to a file avoids building a 700 MB array in memory. This change alone cut peak heap by 2.4 GB.

Step 3: Shrink Global Data

The 186 MB catalogue was loaded by _data/catalogue.json and used by 140 product pages, each of which needed one product. Global data is in the cascade for every template, so it was retained and merged into the data of all 12,400 pages' contexts lazily but still held as one huge object. Two fixes:

  1. Pre-split the data. A prebuild script splits the export into one small JSON file per product under _data/products/, and a paginated template generates the 140 pages from a lightweight index file containing only IDs, names and slugs (310 KB).
  2. Load per page. Each product page loads its own file in a JavaScript data file keyed by the pagination item, so only 140 small objects are ever read.
// products/product.11tydata.js
import fs from 'node:fs/promises';
export default {
  eleventyComputed: {
    product: async (data) => JSON.parse(await fs.readFile(`data/products/${data.item.slug}.json`, 'utf8')),
  },
};
Peak resident memory of the Eleventy build after each change Paired bars comparing peak resident memory before and after each change. Stop reading templateContent across collections, 5.3 to 2.9 gigabytes. Split global catalogue data, 2.9 to 1.8. Stream the search index, already counted. Upgrade from Eleventy 2.0 to 3.0, 1.8 to 1.6. Peak memory after each change Before change After change No cross-collection templateContent 5.3 GB 2.9 GB Split catalogue data 2.9 GB 1.8 GB Eleventy 2.0 to 3.0 1.8 GB 1.6 GB
Each row is cumulative; measured with /usr/bin/time -v as maximum resident set size on the CI runner.

Step 4: Watch Out for Computed Data and Filters That Cache

Two smaller leaks turned up after the big ones were gone. A custom excerpt filter memoised its results in a module-level Map keyed by the input string, so every article's full content stayed referenced by the cache — the fix was to key by page.inputPath and store only the 300-character excerpt. And an eleventyComputed property that built a table of contents parsed the content with a full DOM library for every page, creating short-lived garbage that pushed peak memory up during rendering bursts; switching to a regex-based heading extractor on the Markdown source removed about 150 MB of peak.

Step 5: Size the Heap for Headroom, Not Survival

With peak memory at 1.6 GB, the default heap is enough, but the build still sets an explicit limit so failures are deterministic and well below the runner's physical memory:

- run: npx @11ty/eleventy
  env:
    NODE_OPTIONS: --max-old-space-size=3072

3 GB gives almost 2× headroom over the measured peak while leaving 4 GB of the runner for the OS, file cache and any parallel steps. A build-time regression check records peak RSS on every main-branch build and warns when it grows more than 20% week over week, as in Measuring Build-Time Regressions in CI.

Measured Impact

MeasureBeforeAfterTool
Peak resident memory5.3 GB (with 8 GB heap)1.6 GB/usr/bin/time -v
Build with default heapFails at ~2.5 minSucceedsGitHub Actions
Full build time3 min 41 s2 min 06 sCI logs
Search index file41 MB22 MBls -l
Global data in cascade186 MB0.3 MB indexdu -sh

Build time fell as a side effect: less memory meant far fewer full garbage collections, which had been consuming about 35 seconds of the old build.

Diagnosis steps from out-of-memory error to fix Five steps. Raise the heap temporarily so the build completes. Take a heap snapshot near the end of the build. Group retained memory by retainer. Fix the largest retainer, usually templateContent loops or large global data. Measure peak resident memory again and repeat until there is headroom. Diagnosing an Eleventy out-of-memory error Raise heap temporarily build completes Heap snapshot near end of build Group by retainer largest first Fix the retainer templateContent, global data Measure again repeat to headroom
Raising the heap is step one of the diagnosis, not the fix.

Pitfalls & Rollback

  • Only raising max-old-space-size. The next 10% of content growth breaks the build again, or the runner swaps and builds take three times as long.
  • Reading templateContent in loops over large collections. Use data fields, or read output in eleventy.after.
  • Huge global data files. Split them and load per page.
  • Module-level caches in filters. Key them narrowly and store only what is needed.
  • Rollback: each change is independent. The search index can return to a template and the catalogue to global data at any time; the memory cost returns with them.

Conclusion

Eleventy holds the whole site in memory, so out-of-memory errors come from anything that multiplies per-page data across the site: templates that read every item's rendered content, global data used by a fraction of pages, and caches that keep content alive. On the 12,400-page knowledge base, removing those took peak memory from 5.3 GB to 1.6 GB and the build from 3 minutes 41 seconds to 2 minutes 6 seconds, on the default heap.

FAQ

Is raising max-old-space-size enough to fix an Eleventy out-of-memory error?

It buys time, but it does not fix the cause. Memory use on a growing site keeps rising until it hits the new limit or the CI runner's physical memory, at which point the build fails again or the runner starts swapping. Raise the heap to unblock the build, then find what is holding memory, which is usually collections that store full rendered content or global data loaded into every template.

Why does Eleventy use so much memory on large sites?

Eleventy keeps the data cascade and every collection item in memory for the whole build, because any template can reference any collection. Each item holds its front matter, its raw input, and after rendering its output content. On a 12,000-page site with templates that read templateContent from collections, that output is held several times over.

Does Eleventy 3 use less memory than Eleventy 2?

In the measurements in this guide, Eleventy 3.0 used about 12 percent less peak heap on the same site, mostly from leaner internal structures and ESM loading. The larger savings came from changing templates and data, which apply to both versions.

How do I find which template is using the memory?

Take a heap snapshot during the build with node inspect-brk and Chrome DevTools, or run the build with sampling heap profiler flags, and look at retained size by constructor. Strings holding rendered HTML and large arrays from global data files are the usual culprits. Building a subset of the site with a smaller input directory and comparing peak memory also narrows it down quickly.

Filed underFixesEleventy