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:
| Check | Command | Result |
|---|---|---|
| Peak heap with a raised limit | NODE_OPTIONS=--max-old-space-size=8192 /usr/bin/time -v npx @11ty/eleventy | 5.3 GB resident, build 3 min 41 s |
| Pages and collection sizes | npx @11ty/eleventy --dryrun with a log in eleventy.after | 12,400 pages, articles 11,900 items |
| Global data size | du -sh _data | 186 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.
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 touchingtemplateContentat 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.afterevent 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:
- 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). - 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')),
},
};
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
| Measure | Before | After | Tool |
|---|---|---|---|
| Peak resident memory | 5.3 GB (with 8 GB heap) | 1.6 GB | /usr/bin/time -v |
| Build with default heap | Fails at ~2.5 min | Succeeds | GitHub Actions |
| Full build time | 3 min 41 s | 2 min 06 s | CI logs |
| Search index file | 41 MB | 22 MB | ls -l |
| Global data in cascade | 186 MB | 0.3 MB index | du -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.
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
templateContentin loops over large collections. Usedatafields, or read output ineleventy.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.
Related
- Parent: Astro vs Eleventy for Documentation Sites — memory and build behaviour at scale.
- Astro vs Eleventy Build Times at 10,000 Pages — how both generators scale.
- Content Collections vs the Eleventy Data Cascade — where data lives during a build.
- Upgrading Eleventy 2 to 3 With ESM Configuration — the upgrade that saved the last 200 MB.
- Reducing Hugo Memory Usage on CI Runners — the same problem in Hugo.