A close-up portrait of a man with a salt-and-pepper beard wearing a white collared shirt against a textured beige background.
A close-up portrait of a man with a salt-and-pepper beard wearing a white collared shirt against a textured beige background.

The test suite that kept running out of memory

Jesse James Richard
|
Feb 21, 2026
|
6 min read
#Testing
#CI/CD

For the past week and a half, my test suite has been dying of something I cannot diagnose. I am writing this mid-shitstorm, because the shape of it is one every engineer eventually stumbles into and the honest version is more useful than the tidy one.

Giant Context ships everything through its test suite. Several hundred test files gate every push. The pipeline will not deploy a service until the suite is green. So when the suite starts dying randomly, the whole company is standing behind it in line.

The symptom

The suite runs, climbs to around the twenty-minute mark, and then a test worker prints this and drops dead:

The out-of-memory crash

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

The runtime killed the process for asking more memory than it is allowed. The suite restarts, climbs another twenty minutes, and sometimes finishes and sometimes dies again. A deploy needs the suite green on the feature branch, then on dev, then on prod, so that one unlucky day prices a single change at forty minutes an environment. Two hours of watching a progress bar to ship a fix I wrote in ten minutes.

The irregularity was the maddening part. It failed on GitHub's runners often and locally almost never. Locally I am on a machine with memory to spare, which made the out-of-memory theory feel wrong even while the error message insisted on it. But GitHub was where it hurt. Its evidence is what I chased.

The chase

The commit log of that week records the swings.

Vitest, my test runner, can spread test files across separate processes, called forks, or across threads inside fewer processes. Which one you choose changes how memory adds up. That setting moved twice in four days. In between, the memory limit went up, which treats the symptom and names no cause, and the entire UI package got mocked with a JavaScript Proxy, a single object that pretends to be every component at once. The Proxy lived about six hours.

Feb 13

Forks pool removed

Vitest's process-per-file mode came out (561116b76).

Feb 13

Memory limit raised

More heap for the pre-push suite (44369db83). The symptom, not the cause.

Feb 13

The whole UI, mocked

One JavaScript Proxy pretending to be every component (7b6aff02e).

Feb 13

The Proxy replaced

Six hours later, something less clever and more correct (fad3275e9).

Feb 16

Back to the default pool

The client tests reverted the week's own change (06cdd78c3).

Every one of these changes looked reasonable. None of them moved the failure rate much. That is what a misdiagnosis feels like from the inside. The moves are locally sound. The target is wrong. The log fills with reverted convictions.

What the barrel was loading

The change that survived is the one whose file explains itself. My test mock for the UI package now opens with this:

The barrel mock

// packages/ui/__mocks__/index.ts/** * Lightweight mock for @giantcontext/ui * * Re-exports REAL components from individual MUI paths instead * of loading the full barrel (which pulls in ALL of @mui/material, * MRT, DataGrid, DatePickers, charts, and 50+ custom components). * * Heavy modules (MRT, DataGrid, charts, DatePickers) are stubbed — * tests that need them should mock locally. */export { default as Accordion } from "@mui/material/Accordion";export { default as Alert } from "@mui/material/Alert";// …

A barrel file is a package's front index, one file that re-exports everything the package offers so consumers can import from one place. I enforce barrels everywhere in this codebase as a matter of doctrine. This is what the doctrine costs. When a test file imports one button from the UI package's barrel, the barrel loads everything it exports. All of the component library, the data grids, the chart library, fifty-some custom components, into that test worker's memory. Across every test file that touches any component, times every worker, the suite was carrying the whole UI universe hundreds of times over.

The mock kills the multiplication. Tests that import the UI package get this file instead, which hands back real components fetched from their individual paths, one by one, and cheap stubs for the heavy machinery. The imports stay honest and the memory stays flat. No Proxy cleverness is required. In test workers, memory dies by import graph. The barrel is the fattest node in that graph.

Still undiagnosed

The status from here is uncomfortable. The suite is far more stable than it was two weeks ago. Caching keeps repeat runs cheap and the barrel mock keeps workers thin. The pool settings have settled where they belong. Close to bulletproof, most days.

And I still do not know what the problem is. Nothing I did found it. Everything I did made the suite strong enough to live with it, which is not the same thing. Pretending otherwise is how this kind of bug comes back. Something in this codebase still leaks or loops under memory pressure, showing itself only on small machines under full load. It is still unfound.

Three lessons hold up from here. When out-of-memory strikes irregularly, check what your imports are loading before you raise any memory limit. Mine was pulling the entire UI library into every test worker through one innocent-looking import, which no limit was going to survive. Write down why every surviving config setting exists, because in six months nobody remembers why the pool is set the way it is and whoever cleans it up brings the problem back. And when the failures stop before the diagnosis arrives, say so out loud, in the log and to yourself. A bug that went quiet and a bug that died are different things.

Added August 2026. The diagnosis arrived six months later, and it was arithmetic. Each test worker was carrying a six-gigabyte heap on a runner with seven gigabytes, so exactly one worker fit and a suite built to run in parallel was running one file at a time. That is what "only on small machines under full load" meant. Moving the job to a larger runner and capping the workers took it from eighteen minutes to five, with no test changed. The full account is here, along with the part I got wrong in the meantime: the caching mentioned above turned out to be replaying old results rather than running the tests.

Multitenant serving and cache invalidation

#Architecture

A request arrives from a stranger and becomes a page, the site's owner saves a change, and the next request proves the change landed. One page, two vi...

Jesse James Richard

|

Feb 19, 2026
Read previous

Building something like this

I'm Jesse. I build platforms end to end, and I'm open to work. If this is the kind of engineering you need, get in touch.

Contact Jesse
Home
About
Contact
Sitemap
Privacy Policy
Terms of Service
Cookie Policy

Some posts on this site may be updated on occasion.

The test suite that kept running out of memory | Jesse James Richard