I was building one of my wallpaper apps the other day when the bundle got almost ninety percent of the way through and the terminal suddenly went red: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory. Restarting didn't help — it died at the same point every time. I run six wallpaper apps in parallel, and only the ones whose asset count had crept past a certain size ran into this.
The important thing to understand is that this error is not telling you your code is wrong. It is telling you that Node.js ran out of the memory it was given. Because the cause sits outside your source code, staring at the message alone rarely points you anywhere useful. Below I'll walk through where to make the fix — for both the local Metro bundler and EAS Build — so you can recover with the least friction.
Isolate the symptom and when it happens
There are three classic moments this error appears: during bundling with npx expo start, during an eas build cloud build right before the Gradle/Xcode stage while the bundle is generated, and while writing the production bundle with expo export.
What they share is a single moment where a large number of JavaScript modules get expanded at once. Node.js ships with a default heap ceiling (roughly 2GB to 4GB depending on version and environment), and a large app with many assets hits that ceiling first. In a sense, this error is proof that your app has grown.
Your first move is to confirm it really is a memory problem. Raise the ceiling temporarily before the build command, and if it passes, you have your answer.
# Temporarily raise the heap ceiling to 8GB and see if it still fails
NODE_OPTIONS=--max-old-space-size=8192 npx expo start --clearIf it stops dying, memory was the cause. If it still fails even with this, you need to suspect something else — a circular import or an infinite loop, for example.
Fix 1: Raise the local heap ceiling permanently
Typing the environment variable every time isn't realistic, so bake it into your package.json scripts. That keeps the behavior consistent across machines and teammates, and avoids forgotten flags.
{
"scripts": {
"start": "NODE_OPTIONS=--max-old-space-size=8192 expo start",
"export": "NODE_OPTIONS=--max-old-space-size=8192 expo export"
}
}On Windows the NODE_OPTIONS=... prefix won't work, so wrap it with cross-env to make it portable across operating systems.
{
"scripts": {
"start": "cross-env NODE_OPTIONS=--max-old-space-size=8192 expo start"
}
}Use roughly half your installed RAM as the target. On a 16GB machine, 8192 (8GB) is a safe figure; pushing past your physical memory triggers swapping and actually slows things down. On my Mac it stabilized at 8192, and raising it further made no difference.
Fix 2: For EAS Build, raise the resource class
If the failure happens in the cloud build rather than locally, setting NODE_OPTIONS on your own machine does nothing. You need more memory on the EAS Build machine itself. Set resourceClass on each profile in eas.json.
{
"build": {
"production": {
"android": { "resourceClass": "large" },
"ios": { "resourceClass": "large" },
"env": {
"NODE_OPTIONS": "--max-old-space-size=8192"
}
}
}
}The key here is to raise the machine's memory with resourceClass while also lifting Node's heap ceiling through env. Bumping only the machine memory still leaves Node capped low, so the build dies at the same spot. Both have to move together for the fix to take. Note that large may fall outside the free tier, so check your billing plan first.
Fix 3: Reduce what goes into the bundle in the first place
Adding memory is treating the symptom. If your app is going to keep growing, structurally reducing the number of modules in the bundle pays off longer. Enabling inline requires in metro.config.js keeps the number of modules loaded at once lower.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.transformer.getTransformOptions = async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
});
module.exports = config;For image assets, keep large PNGs out of the JS bundle by swapping any require-referenced images for optimized versions. When I moved my wallpaper apps' high-resolution images to remote delivery, the memory the bundler consumed dropped visibly. Not hoarding assets locally is, in my experience, the single most effective long-term prevention.
If it dies in Android's Gradle step, suspect a different ceiling
Here's a confusing one: an Android build can throw a similar out-of-memory error that is actually Gradle (the JVM) running out of heap, not Node.js. If the log says Java heap space or GC overhead limit exceeded rather than JavaScript heap, you're raising the wrong ceiling. In that case, adjust the JVM arguments in android/gradle.properties.
# android/gradle.properties
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024mNo amount of bumping NODE_OPTIONS will touch this. Just remember that one word in the error message — JavaScript versus Java — tells you where to fix it, and you'll avoid a lot of wasted trial and error. Early on I confused the two and burned time poking at the Node side. Once I trained myself to read the subject of the log line first, recovery got much faster.
Habits that keep it from coming back
This error tends to show up unannounced precisely when your app is doing well. I've developed apps solo since 2014, and on the way to a cumulative 50 million downloads I hit this same wall every time features and assets piled up. That's why I now put NODE_OPTIONS into the CI build profile from day one, and make a habit of checking the change in bundle size whenever I add assets.
As your next step, add NODE_OPTIONS=--max-old-space-size=8192 locally and see whether the build gets through. If it does, all that's left is folding it into the permanent settings above, and you'll stay clear of this error for a good while. I hope this helps if you've been stuck watching it fail at the same spot.