How to Fix react-scripts Build Hanging Forever on 'Creating an Optimized Production Build' in a Large Monorepo
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 15–45 mins
TL;DR
- What broke:
react-scripts buildenters an infinite Webpack compilation loop or starves on worker threads when traversing a monorepo's symlinkednode_modules, shared packages, or unboundedsrcscope. - How to fix it: Scope Babel/Webpack to your app's actual source, disable symlink resolution, cap Terser/source-map worker threads, and optionally eject or migrate to CRACO/Vite.
- Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
package.jsonand any custom webpack config and get a patched output without sending secrets anywhere.
The Incident (What Does the Error Mean?)
Your CI runner or local terminal prints:
Creating an optimized production build...
…and then nothing. No progress bar. No error. CPU pegged at 100% on one core, or dead silent. The process never exits. react-scripts version doesn't matter — this hits 4.x and 5.x equally hard.
Immediate consequence: Your deployment pipeline is blocked. Every retry burns CI minutes. Engineers assume it's a flaky runner and restart — making it worse.
The Attack Vector / Blast Radius
This is a cascading resource exhaustion, not a single bug. Three independent failure modes all produce the same symptom:
1. Symlink resolution loop (most common in monorepos)
Webpack 5 (bundled in react-scripts 5.x) follows symlinks by default. In a Yarn/npm workspace, packages/ui/node_modules/react is a symlink back to root/node_modules/react. Webpack traverses it, finds more symlinks, and enters an O(n²) resolution spiral. With 50+ workspace packages this never terminates.
2. Unbounded ModuleScopePlugin bypass + full node_modules Babel transpilation
If any package in your monorepo imports across workspace boundaries, CRA's ModuleScopePlugin throws, and the common workaround (SKIP_PREFLIGHT_CHECK=true + babel-loader include patches) accidentally tells Babel to transpile all of node_modules. On a monorepo with 2,000+ packages this is effectively infinite work.
3. Terser + source-map worker thread starvation
react-scripts spawns os.cpus().length Terser workers. On a shared CI runner (e.g., GitHub Actions 2-core), spawning 32 workers (because /proc/cpuinfo reports the host's physical cores) causes the kernel to thrash. The build appears hung but is actually livelock-swapping worker state.
Blast radius: Every downstream service depending on this build artifact is blocked. If this is a shared component library in the monorepo, every consuming app's pipeline stalls too.
How to Fix It (The Solution)
Basic Fix — Environment Variables (No Eject)
Add to your app's .env.production or inline in the build script:
- "build": "react-scripts build"
+ "build": "GENERATE_SOURCEMAP=false INLINE_RUNTIME_CHUNK=false react-scripts build"
And in .env:
+ GENERATE_SOURCEMAP=false
+ SKIP_PREFLIGHT_CHECK=true
Disabling source maps eliminates the source-map worker pool entirely and cuts Terser work by ~60%.
Enterprise Best Practice — CRACO + Webpack Hardening
Install CRACO: npm install -D @craco/craco
package.json:
- "build": "react-scripts build"
+ "build": "craco build"
craco.config.js (new file at app root):
+ const path = require('path');
+ const TerserPlugin = require('terser-webpack-plugin');
+
+ module.exports = {
+ webpack: {
+ configure: (webpackConfig) => {
+
+ // FIX 1: Disable symlink resolution to prevent monorepo traversal loops
+ webpackConfig.resolve.symlinks = false;
+
+ // FIX 2: Scope Babel transpilation strictly to THIS app's src
+ const babelLoader = webpackConfig.module.rules
+ .find(r => r.oneOf)
+ .oneOf.find(r => r.loader && r.loader.includes('babel-loader'));
+ if (babelLoader) {
+ babelLoader.include = path.resolve(__dirname, 'src');
+ }
+
+ // FIX 3: Cap Terser worker threads to avoid CI livelock
+ webpackConfig.optimization.minimizer = [
+ new TerserPlugin({
+ parallel: 2, // Hard cap. Do NOT use true in monorepo CI.
+ terserOptions: {
+ compress: { passes: 1 },
+ },
+ }),
+ ];
+
+ // FIX 4: Exclude all workspace sibling packages from module resolution scope
+ webpackConfig.resolve.modules = [
+ path.resolve(__dirname, 'src'),
+ 'node_modules',
+ ];
+
+ return webpackConfig;
+ },
+ },
+ };
If you're on Yarn Workspaces and still seeing symlink loops, add to root package.json:
+ "installConfig": {
+ "hoistingLimits": "workspaces"
+ }
This prevents hoisting from creating the circular symlink chains Webpack chokes on.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Nuclear Option — Migrate to Vite
If CRACO still hangs (common in monorepos with 100+ packages), CRA is the wrong tool. Vite's native ESM dev server and Rollup production build handle workspace symlinks correctly by design.
- "react-scripts": "5.x"
+ "vite": "^5.0.0"
+ "@vitejs/plugin-react": "^4.0.0"
Migration is ~2 hours for a standard CRA app. The build time reduction is typically 10x.
Prevention in CI/CD
1. Enforce build timeouts — fail fast, don't hang:
# GitHub Actions
- name: Build
timeout-minutes: 10
run: npm run build
A hanging build that kills itself in 10 minutes is infinitely better than one that burns 60 minutes of CI quota silently.
2. Cache node_modules with a lockfile hash, not a date:
- uses: actions/cache@v4
with:
path: '**/node_modules'
key: ${{ runner.os }}-nm-${{ hashFiles('**/package-lock.json') }}
Stale caches with broken symlinks are a primary cause of first-run hangs.
3. Add a Webpack build performance budget lint step:
Use webpack-bundle-analyzer or bundlesize as a required CI check. If the bundle analysis step itself hangs, you've caught the problem before it blocks deployment.
4. Pin react-scripts and audit Webpack config on upgrades:
react-scripts 5.x silently upgraded to Webpack 5 with changed symlink defaults. Use npm audit + Dependabot with a mandatory staging build gate before merging major CRA version bumps.
5. Consider DISABLE_ESLINT_PLUGIN=true in CI:
+ DISABLE_ESLINT_PLUGIN=true
CRA's ESLint plugin runs in-process during build and in large monorepos can itself cause the hang before Webpack even starts. Separate linting into its own CI step.