Initializing Enclave...

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 below 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 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.

Related Diagnostics

"Part of the Performance Utility Matrix."

View all 219 Performance Tools →