# Diagnose and fix Netlify build failures: classify the failing stage first
Use this when a Netlify deploy fails during the build. The single most useful
line in the deploy log is the stage marker, because each stage fails for
different reasons:
```
Failed during stage "building site": Build script returned non-zero exit code: 2
```
## 1. Find which stage failed
Open the deploy log and find the `Failed during stage "..."` line.
- **`"building site"`** — your build command (or its environment) failed. Everything
in section 2 applies.
- **`"deploying site"`** — the build succeeded but deploy-time processing failed
(oversized function bundles, form/redirect processing). See the functions
skill for bundle-size issues.
- **Earlier stages** (`"preparing repo"`, `"initializing"`) — the problem is
upstream of your code: a bad base directory, missing repo access, or invalid
config. Read the lines immediately above the failure.
## 2. Classify by the error signature
Match the log's distinctive text to one of these before you touch config.
### A. "Base directory does not exist" or a missing publish directory
```
Base directory does not exist: /opt/build/repo/web
```
**Cause:** the base directory in build settings doesn't match a directory in the
repo. The base directory is where Netlify looks for `package.json` / `.nvmrc`,
installs dependencies, and runs the build command.
**Fix:** set base to the real subdirectory. Then recheck **publish**: it is
resolved *relative to the base directory*, not the repo root. The classic
double-up: base `web` plus publish `web/public` makes Netlify look for
`/opt/build/repo/web/web/public`. With base `web`, publish should be just
`public`.
### B. JavaScript heap out of memory, or "Killed"
```
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
```
```
10:14:05 AM: /opt/build-bin/build: line 77: 1351 Killed [... details about the build]
```
**Cause:** the build tried to allocate more memory than the build container has.
Gatsby builds hit this most often; anything bundling a huge dependency tree can
too. This is an infrastructure limit, not a bug in your code.
**Fix:** raise Node's old-space heap via a `NODE_OPTIONS` environment variable.
Per the Netlify support guide, `--max-old-space-size=4096` is a common value
that works — set it in site settings under Environment variables, or in
`netlify.toml`:
```toml
[build.environment]
NODE_OPTIONS = "--max-old-space-size=4096"
```
Also: reduce parallel workers (parallel webpack/Gatsby workers multiply memory
contention without helping on a small build container), and if the site is
simply huge, Netlify's High-Performance Builds is the paid escape hatch.
### C. Your build command exited non-zero
```
Failed during stage "building site": Build script returned non-zero exit code: 2
```
**Cause:** your command failed. Scroll up for *your* tool's error — the stage
line is only the summary. Two Netlify-specific traps to check first:
1. **`CI` is set.** Netlify runs builds with `CI=true` (the log shows
`$ CI= npm run build`). Some frameworks treat warnings as errors under CI —
`react-scripts build` (create-react-app) is the classic case: warnings that
pass locally fail the Netlify build. Either fix the warnings or override the
behavior in build settings.
2. **Node version mismatch.** Engines incompatibility, or syntax errors on code
that runs fine locally, means the build used the wrong Node. Pin it with a
`NODE_VERSION` variable (or a `.nvmrc` file, or `package.json` engines):
```toml
[build.environment]
NODE_VERSION = "20"
```
### D. Environment variable missing at build time
Symptoms: `ReferenceError: SOME_VAR is not defined`, or API calls returning
401/404 during the build.
**Causes, in order of likelihood:**
1. The variable was added or changed *after* the last deploy — env vars are
baked in at build time, so you must trigger a new deploy.
2. Wrong deploy-context scoping: works on production but fails on deploy
previews (or vice versa) — check which contexts the variable is enabled for.
3. A `NETLIFY_`-prefixed name — that prefix is reserved and the UI rejects it
(except `NETLIFY_AUTH_TOKEN` / `NETLIFY_SITE_ID`, which are CLI-local, not
site variables).
## 3. The build config reference
The real keys in `netlify.toml`:
```toml
[build]
command = "npm run build" # build command
publish = "dist" # output dir, relative to base
base = "" # repo subdirectory to build from
functions = "netlify/functions" # serverless function sources
edge_functions = "netlify/edge-functions"
ignore = "git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF -- src/"
[build.environment]
NODE_VERSION = "20"
[context.deploy-preview]
command = "npm run build:preview" # per-context overrides
```
One rule that bites people: **settings in `netlify.toml` override the same
settings in the Netlify UI.** If a value lives in both places, the file wins —
keep it in one place.
To reproduce the build locally in the same environment, run `netlify build`
(Netlify CLI) from the repo root.
## 4. Checklist
1. Read the `Failed during stage` line first — only then scroll up for the real error.
2. Base/publish: `Base directory does not exist` → fix base; then fix publish
(relative to base, not repo root).
3. OOM / `Killed` → `NODE_OPTIONS` with `--max-old-space-size=4096`; reduce parallelism.
4. Non-zero exit → find your tool's error; check the `CI=` warnings-as-errors
trap; pin `NODE_VERSION`.
5. Missing env var → redeploy after adding it; check context scoping; don't use
a `NETLIFY_` prefix.
6. `netlify.toml` overrides the UI — don't configure the same setting in both places.