You bumped a dependency, ran npm install locally, the local build looks great, but pushing to Cloudflare Pages still ships the old version — or the build “succeeds” while production shows pre-change output. Cloudflare Pages restored its cached dependencies from the previous build instead of installing the new ones.
Fastest fix (works ~80% of the time): make sure the updated package-lock.json is committed and pushed, then clear the build cache in the dashboard and redeploy. In the dashboard go to Workers & Pages → your project → Settings → Build → Build cache → Clear Cache, then Deployments → Retry deployment. The rest of this article explains how to confirm which bucket you are in and how to stop it recurring.
How the cache actually works (June 2026)
Two facts about the current (V3) build system explain almost every stale-deploy report:
- Pages does not cache your
node_modulesfolder verbatim. It caches the package-manager cache directory (.npmfor npm,.cache/yarn,.pnpm-store,.bun/install/cache) plus framework build output such as.next/cache. On a cache hit it restores those, then still runs the install step — but a strict install reads your committed lockfile, so if the lockfile is stale, you get stale deps. - With a committed lockfile, Pages already runs the strict install (
npm ci,yarn --frozen-lockfile/--immutable,pnpm install --frozen-lockfile) automatically — you do not have to set that yourself. So a stale deploy is almost never “Pages ran a loose install”; it is “the lockfile Pages installed from is the old one,” or “the wrong build image / Node version compiled the wrong artifact.”
Cache retention, for reference: a project’s build cache is purged seven days after its last read (and the per-project cache cap is 10 GB), so simply waiting a week is a slow accidental fix.
Which bucket are you in?
| Symptom in the build log | Most likely cause | Jump to |
|---|---|---|
Install step finishes in ~1s, up to date | Stale/uncommitted lockfile, cache restored | Causes 1–2, Steps 1–2 |
npm ci errors lock file ... can only install with an existing package-lock.json or out of sync | Lockfile missing or out of sync | Cause 2, Step 2 |
| Build line 1 shows an unexpected Node version | Build image / Node version drift | Cause 3, Step 3 |
| Native module crashes at runtime, build “passed” | Cached binding from old Node major | Cause 3, Steps 1+3 |
| Command at top of log isn’t what you set | Build command / preset mismatch | Cause 4, Step 4 |
| Uploaded file list matches the previous deploy | Output dir reused | Cause 5, Step 5 |
Common causes
Ordered by hit rate.
1. The cached dependencies were restored against a stale lockfile
By far the most common. You bumped a dep in package.json from ^1.0.0 to ^2.0.0, local npm install rewrote package-lock.json, but you pushed without committing the lockfile. Pages restores its dependency cache and installs from the lockfile that is actually in the repo — still the v1 one — and ships v1.
In the build log:
Restoring from build cache...
Success: Finished restoring build cache
Installing project dependencies: npm clean-install
up to date in 1s
up to date in 1s is the smoking gun — no real install happened because the committed lockfile already matches the restored cache.
How to spot it: Locally, git status shows package-lock.json uncommitted; in Pages logs the install step finishes near-instantly with no package downloads.
2. package-lock.json not committed, ignored, or duplicated
Some repos accidentally .gitignore the lockfile, or commit several lockfiles (npm + yarn + pnpm) and Pages picks the wrong one. Without a committed lockfile, npm ci fails outright rather than silently installing the wrong thing.
How to spot it: grep -E "lock|node_modules" .gitignore to confirm the lockfile isn’t ignored; ls *.lock* package-lock.json 2>/dev/null to see which lockfiles actually exist.
3. Build image / Node version drift compiled the wrong artifact
As of June 2026 the current V3 build image defaults to Node 22.16.0 on Ubuntu 22.04. The old trap — Pages defaulting to Node 18 — now only bites projects still pinned to the v2 build image (Node 18.17.1). v1 projects auto-migrate to V3 on 2026-09-15 and v2 projects on 2027-02-23, so a project that has sat untouched can still be on the old default. Native deps like better-sqlite3 or sharp compile different bindings per Node major; a binding cached under one Node version won’t match another, so the build “succeeds” but the runtime crashes.
How to spot it: The first lines of the Pages build log print the build-image version and the Node version. Compare to your local node -v. Check the pinned image at Settings → Build → Build system version.
4. The build command isn’t the one you think
Build command is npm run build, but a framework preset prepends its own step, or you changed the command in env vars while the deploy still uses the command saved in project settings.
How to spot it: The top of the build log shows the literal command being executed; compare it to Settings → Build → Build command.
5. Output dir is also cached
Rarer, but if a custom build script copies the previous output forward (or your framework’s build-output cache like .next/cache masks a change), Pages can publish stale assets.
How to spot it: Compare the “Uploading” / “Deploying” file list in the build log to your local build output (dist/).
Shortest path to fix
Step 1: Commit the lockfile, then clear the build cache and cold-deploy
First make sure the new lockfile is actually in the repo (Step 2 below), then in the dashboard:
Workers & Pages → your project → Settings → Build → Build cache → Clear Cache
Then Deployments → Retry deployment to trigger a fully cold build. The log should show:
No build cache found
Installing project dependencies: npm clean-install
added 487 packages in 32s
added N packages in Ns is much slower than the old up to date in 1s — that’s proof a real install happened.
Step 2: Verify package-lock.json is committed and in sync
# Force-regenerate the lockfile locally
rm -rf node_modules package-lock.json
npm install
# Make sure it's in git and pushed
git status package-lock.json
git add package-lock.json package.json
git commit -m "sync lockfile"
git push
.gitignore check:
grep -E "package-lock|yarn.lock|pnpm-lock" .gitignore
# Expected: no output, or those lines are commented
Keep only one lockfile. If you use pnpm, delete package-lock.json and yarn.lock; keep pnpm-lock.yaml.
Step 3: Pin the Node version and confirm the build image
Pages reads the Node version from project files first, then env vars. In priority order:
# Option 1: .nvmrc at project root (recommended — works locally and in CI)
echo "22.16.0" > .nvmrc
git add .nvmrc
# Option 2: NODE_VERSION env var in the Pages dashboard
NODE_VERSION = 22.16.0
Note: in the V3 build system the package.json engines.node field is not read for Node selection — use .nvmrc / .node-version or NODE_VERSION. If your project is still on an old image, switch it at Settings → Build → Build system version (choose V3). The next build’s first lines should show the pinned version, matching local.
Step 4: Confirm the install/build command
You usually do not need to change the install step — with a committed lockfile Pages already runs npm ci. What you control is the build command and output directory:
Build command: npm run build
Build output directory: dist
If you want install failures to be loud and explicit, you can set the build command to npm ci && npm run build; npm ci errors when the lockfile is out of sync instead of silently using stale deps. Keep the build output directory matching your framework (dist for Astro/Vite, build for CRA, .output/public for Nuxt, etc.).
Step 5: Verify the artifact and cache behavior
# Build once locally to know what the hashed filenames should be
npm ci && npm run build
ls -la dist/
# After deploy, fetch a hashed asset and confirm it's the new version
curl -sI https://your-project.pages.dev/_astro/main.HASH.js
If the hashed filename on the new deploy differs from the previous one, the artifact really updated. If it’s identical, the output dir is being reused — go back to Step 1 and clear the cache again.
How to confirm it’s fixed
- The build log shows
No build cache found(orRestoring from build cachefollowed byadded N packages, notup to date in 1s). - Build line 1 shows your pinned Node version.
- The deployed app reflects the change — hard-refresh (
Cmd/Ctrl+Shift+R) orcurla hashed asset to bypass the CDN edge cache, which is separate from the build cache.
Prevention
- After upgrading any meaningful dep (especially ones with native bindings), clear the build cache once in the dashboard.
- Always commit and push
package-lock.jsonin the same commit aspackage.json; the strict install Pages runs depends on it. - Pin Node with
.nvmrc(and keep the project on the V3 build image) so default-Node bumps can’t surprise you. - Keep exactly one lockfile in the repo (npm OR yarn OR pnpm — pick one).
- Add a CI check that the lockfile is up to date:
npm install --package-lock-only && git diff --exit-code package-lock.json.
FAQ
Is the build cache the same as Cloudflare’s CDN cache?
No. The build cache lives in the CI build environment (package-manager cache + framework build output). The CDN edge cache serves your deployed assets to visitors. A stale build needs the dashboard Clear Cache; a stale page in the browser needs a CDN purge or a hard refresh.
I clicked Clear Cache but the next build still says up to date. Why?
The cache was cleared but the lockfile Pages installs from is still the old one. Clearing the cache doesn’t change your committed lockfile. Do Step 2 (regenerate + commit + push the lockfile) first, then clear and redeploy.
Where did the “Builds & deployments” tab go?
It was reorganized. As of June 2026 the build settings (build command, output directory, build cache, build system version) live under Settings → Build for the project.
Why does my native module work locally but crash on Pages?
Local and Pages are likely on different Node majors, so the cached native binding doesn’t match. Pin the same Node version with .nvmrc, clear the build cache, and redeploy so the binding recompiles against the right runtime.
Does Cloudflare run npm install or npm ci?
With a committed lockfile, Pages runs the strict install (npm ci for npm). That’s why committing an up-to-date lockfile is the real fix — a loose npm install is not what runs in CI.
Related
- Deploy succeeded but page shows old content
- Service worker serves stale content after deploy
- Build output size exceeded
- Vercel build failed
- Env var missing in prod
- Astro pages 404 after deploy
Tags: #Hosting #Troubleshooting