SE
STACKENGINE
Zero-Trust Client IDE/ Performance

CRA react-scripts Large Monorepo Build Fix

react-scripts build hangs indefinitely at 'Creating an optimized production build' in large monorepos due to Webpack's file-watching, symlink resolution, and worker thread exhaustion.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(1L)
AI Audit Console
> Waiting for pre-flight audit...
AirLock HYBRID KERNEL: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPECUpdated 2026

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 build enters an infinite Webpack compilation loop or starves on worker threads when traversing a monorepo's symlinked node_modules, shared packages, or unbounded src scope.
  • 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 above to auto-refactor this — paste your package.json and 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 browser-native 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.

Further Exploration / Matrix Routing