The build is where most first-time deploys die, and it’s also the easiest phase to diagnose because every clue is in one place: the deploy log. If you’ve internalised the method (step 02) and the logs cheat sheet (step 03), build failures are mostly pattern matching.
This step is the pattern library. Each section has the log line you’ll see, what it means, the diagnosis, and the smallest fix that disproves the hypothesis.
The diagnosis flow
flowchart TB
start["Build failed"]
q1{"First error<br/>shape?"}
q1 -->|"Module not found /<br/>Cannot find package"| A["Section 1:<br/>Missing dependency"]
q1 -->|"SyntaxError /<br/>incompatible engine"| B["Section 2:<br/>Language version"]
q1 -->|"Lockfile out of date /<br/>frozen-lockfile failure"| C["Section 3:<br/>Lockfile drift"]
q1 -->|"command not found /<br/>permission denied"| D["Section 4:<br/>Build command / tooling"]
q1 -->|"OOM / killed during build"| E["Section 5:<br/>Build resource limits"]
Find the section that matches your first error and jump to it. The patterns at the end of the step are the rarer cases.
1. Module not found / ModuleNotFoundError
The single most common build failure across every language.
What you’ll see
==> Running build command 'npm ci && npm run build'> app@1.0.0 build> next build Creating an optimized production build... ./src/lib/auth.tsModule not found: Can't resolve '@/lib/db'==> Build failed==> Running build command 'pip install -r requirements.txt && python manage.py collectstatic'Successfully installed Django-5.0.0 ...==> Running pre-deploy commandTraceback (most recent call last): ModuleNotFoundError: No module named 'redis'==> Pre-deploy failedWhat it means
One of two things, and the distinction matters:
| Variant | Cause | Where the cause is |
|---|---|---|
| Dependency missing | A package referenced by your code isn’t in package.json / requirements.txt | Your manifest file |
| Path resolution failure | A relative or aliased path doesn’t resolve at build time on Linux | The import statement or your bundler config |
Diagnosis
Check whether the missing module is a third-party package or one of your own files.
- Third-party (
redis,lodash,psycopg2) → it’s not declared as a dependency. Runnpm ls redisorpip show redislocally. If it’s not there, add it. - Your own file (
@/lib/db,./components/foo) → it’s a path issue. The most common cause is filesystem case: macOS and Windows treat./Fooand./foothe same; Linux doesn’t.
The fix
# Nodenpm install --save the-missing-package
# Pythonpip install the-missing-packagepip freeze > requirements.txtFor path issues, the smallest test is to rename the file with git mv to lowercase and reimport:
# DON'T just rename on macOS - git won't see the changegit mv src/components/Foo.tsx src/components/Foo.tsx.tmpgit mv src/components/Foo.tsx.tmp src/components/foo.tsx# Then update imports2. Language / dependency version conflicts
The second-most-common, and the one that produces the most confusing error messages.
What you’ll see
> app@1.0.0 buildSyntaxError: Unexpected token '??=' at internalCompileFunction (node:internal/vm:73:18)==> Build failednpm warn EBADENGINE Unsupported engine {npm warn EBADENGINE package: 'some-pkg@5.0.0',npm warn EBADENGINE required: { node: '>=20.0.0' },npm warn EBADENGINE current: { node: 'v18.20.0' }ERROR: Package 'some-pkg' requires Python >=3.11, but the running Python is 3.10.13.go: go.mod requires go >= 1.22 (running go 1.21.5)What it means
Render’s runtime is using a different language version than your project expects. The “default” version Render picks is documented in language-support, and it drifts over time - what was a fresh default when you set up the service may be older than your dependencies need today.
Diagnosis
Look at the first three lines of any deploy log:
==> Using Node version 18.20.0 (default)==> Using Python version 3.10.13 (default)==> Using Go version 1.21.5 (default)The word (default) is the key signal. If it says (default), you haven’t pinned a version and Render picked one. Compare that against your project’s expected version:
| Source of truth | Lives in |
|---|---|
| Node | engines.node in package.json, .nvmrc, or .node-version |
| Python | runtime.txt (e.g. python-3.12.1) or a .python-version file |
| Ruby | .ruby-version or Gemfile |
| Go | go.mod (the go directive) |
If the project’s expected version differs from what Render is using, you have your cause.
The fix
The cleanest fix is to set the version in a file your project already reads, so the same version is used locally and on Render. Pick one mechanism per language:
20.11.0python-3.12.1If you can’t use a project file (e.g. shared monorepo), set the environment variable in the Render Dashboard or a Blueprint:
services: - type: web name: api runtime: node envVars: - key: NODE_VERSION value: "20.11.0"Available env vars per runtime: NODE_VERSION, PYTHON_VERSION, RUBY_VERSION, GO_VERSION, RUST_VERSION. See Render’s language-support docs for the full table.
3. Lockfile drift
You ran npm install locally, committed the lockfile, but the build still complains. Or your build uses npm ci and explodes.
What you’ll see
npm error code EUSAGEnpm errornpm error `npm ci` can only install packages when your package.json and package-lock.json are in sync.npm error Invalid: lock file's express@4.18.2 does not satisfy express@4.19.0ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with package.jsonERROR: THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE.What it means
Your package.json (or requirements.txt) has been edited but the lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock, Pipfile.lock) hasn’t been regenerated. npm ci, pnpm install --frozen-lockfile, and pip install --require-hashes are strict by design - they refuse to install if the two files don’t agree.
Diagnosis
Look at the date of your last lockfile change vs your last manifest change:
git log -1 --format="%ai %h" -- package-lock.jsongit log -1 --format="%ai %h" -- package.jsonIf package.json is newer than the lockfile, that’s the drift.
The fix
Regenerate the lockfile locally and commit it:
# Noderm package-lock.jsonnpm installgit add package-lock.jsongit commit -m "Refresh lockfile"
# pnpmpnpm installgit add pnpm-lock.yamlgit commit -m "Refresh lockfile"
# Python with pip-toolspip-compile requirements.in --output-file requirements.txtgit add requirements.txtgit commit -m "Refresh pinned requirements"4. Build command / tooling issues
Sometimes the build command itself is the problem.
What you’ll see
==> Running build command 'pnpm install && pnpm build'/bin/sh: pnpm: not found==> Build failed==> Running build command './scripts/build.sh'/bin/sh: ./scripts/build.sh: Permission denied==> Build failed==> Running build command ''==> Build successful==> Deploying...==> No start command specifiedWhat it means and what to do
| Symptom | Cause | Fix |
|---|---|---|
pnpm: not found, yarn: not found | Render’s native runtimes default to npm. pnpm/yarn need to be installed first | Prefix the build command: corepack enable && pnpm install && pnpm build |
Permission denied on a .sh file | The script isn’t executable | git update-index --chmod=+x scripts/build.sh && git commit |
| Empty build command output | The Render Dashboard’s build command field was cleared | Set it back, or define it in render.yaml |
pip-installer not found | Build is using a base image that doesn’t have your toolchain | Switch to Docker or use Render’s native runtime for that language |
For non-Node/Python tooling (Bun, Deno, custom binaries), the native-runtimes tools docs list what’s pre-installed. Anything not on that list, install it as part of your build command:
curl -fsSL https://bun.sh/install | bash && ~/.bun/bin/bun install && ~/.bun/bin/bun run buildOr move to a Dockerfile - at that point you’re managing the toolchain yourself.
5. Build resource limits
Rarer, but worth knowing about because it’s the most confusing failure when you hit it.
What you’ll see
==> Running build command 'npm run build'> next build Linting and checking validity of types ... Creating an optimized production build ...Killed==> Build failedOr the build just hangs, then:
==> Build exceeded maximum allowed time==> Build failedWhat it means
Builds run on a build worker with finite RAM and a time limit. Heavy builds - large TypeScript projects, big bundlers, image processing in the build step - can hit either limit.
Diagnosis
A Killed with no stack trace, especially from next build, vite build, or webpack, is almost always OOM. Confirm by reproducing locally with NODE_OPTIONS="--max-old-space-size=2048" to constrain to a similar memory ceiling.
The fix
A few levers, in order of effort:
- Skip what doesn't need to be in the build Move type-checking out of
next build(SKIP_ENV_VALIDATION=1,NEXT_DISABLE_ESLINT=1, etc.). Run it in CI instead, before deploy. - Cache aggressively Render caches
node_modulesbetween builds. Make sure you’re not invalidating it (changing the lockfile every deploy will). - Move to a Dockerfile Multi-stage builds let you push more work into the build, but you also pay for the build environment yourself. Worth it for large projects.
- Contact support if you genuinely need more If you’ve optimised and still can’t fit, support can advise on plan options.
Two patterns that look like build failures but aren’t
”Build succeeded but my code didn’t update”
==> Cloning from https://github.com/example/app==> Checking out commit 4f2a91c in branch main==> Build successfulThe commit being checked out is the third line. If it’s not the commit you expected, the build did exactly what you asked - Render is just deploying an older commit. Causes:
- The deploy was triggered manually for a specific commit (deploy hook, API call, the Render Dashboard “Deploy specific commit” action).
- Auto-deploy is off and a teammate pushed without triggering a deploy.
- You pushed to a branch that isn’t the service’s deploy branch.
Fix is to trigger a fresh deploy or push the right commit to the right branch.
”Build is succeeding but the wrong service is being built”
In a monorepo, rootDir controls which subdirectory Render builds. If it’s wrong, you’ll build the wrong project and deploy “succeeds” with the wrong artifact.
services: - type: web name: api rootDir: ./apps/api runtime: node buildCommand: npm ci && npm run buildCheck rootDir in the Render Dashboard’s Settings → Build & Deploy section. The path is relative to the repo root.
What you learned
- `Module not found` is either a missing dependency (fix the manifest) or a path/case issue (fix the filename on a case-sensitive filesystem)
- Language version mismatches show up as `SyntaxError`, `EBADENGINE`, or `requires Python >=`. The `(default)` marker in the deploy log's first lines tells you Render picked a version for you
- Lockfile drift is what `npm ci` / `--frozen-lockfile` is *supposed* to catch. Fix the drift at the source by regenerating the lockfile, not by switching to `npm install`
- `pnpm: not found` and similar mean the tool isn't in Render's native runtime - install it in the build command or use Docker
- A `Killed` with no stack trace in a heavy build is almost always OOM. Optimise the build (skip type-checking, cache deps) or move to a Dockerfile
- If the deploy log shows the wrong commit or the wrong service was built, the fix is in the deploy trigger or `rootDir`, not the code