A Stale pnpm-lock.yaml That Broke the Vercel Deploy
Vercel infers the package manager from the lockfile, not package.json. One leftover pnpm-lock.yaml failed the deploy with ERR_PNPM_OUTDATED_LOCKFILE.
Everything passed locally, then Vercel refused to build. The log had one line to say about it: ERR_PNPM_OUTDATED_LOCKFILE, pnpm-lock.yaml out of sync with package.json. Odd, because I barely touch pnpm in this project. But there it was in the frontend root: pnpm-lock.yaml, 9,285 lines, last generated August 12.
Two weeks earlier I had added four dependencies for the blog embed features: echarts, mermaid, recharts, and @teispace/next-themes. All four went in through npm install, neatly recorded in package.json and package-lock.json. pnpm-lock.yaml never got updated, because no pnpm ever ran. The file was a leftover from the project's early days.
Vercel reads the lockfile, not package.json
My first guess was build cache. The dependencies were clearly installed and package-lock.json was fresh. But that is not how Vercel decides: at deploy time it looks for a lockfile in the project and infers the package manager from it. The package managers documentation states this outright — detection from the lock file, not from package.json. pnpm-lock.yaml happened to be there first, so the project was treated as a pnpm project and Vercel ran pnpm install.
One exception: with Corepack enabled and a packageManager field in package.json, Vercel uses that instead. Mine was empty, so lockfile detection won.
Why did the install fail outright instead of printing a warning? Because pnpm runs with the frozen-lockfile flag by default in CI; that rule is in the pnpm install docs. The lockfile must not be touched; if it does not match package.json, the install stops. Even the pnpm version is guessed from the lockfileVersion field inside the file. I never set that field, and the pnpm version Vercel picked from the stale file could differ from what I use locally. This failure pattern is well known: issue vercel/vercel#8272 from 2022 and the ERR_PNPM_OUTDATED_LOCKFILE community thread from late 2025 describe the same case.
The shortest fix: delete the file that lies
The options I weighed:
- Regenerate pnpm-lock.yaml with pnpm so it matches. Correct if you actually want pnpm. But then you maintain two lockfiles for one project. Sensible for a pnpm-first monorepo, not for me.
- Enable Corepack and fill in the packageManager field. The official way to pin npm, but it adds one more setting to maintain and leaves the lying file in the repo.
- Delete pnpm-lock.yaml. This project has been npm from day one — the Dockerfile runs
npm ci, and package-lock.json follows every install. That one file was the only thing telling a different story.
I took the third option. One commit on August 27, minus 9,285 lines, deploy green. The shortest fix I can think of for this class of problem: when a file claims the wrong package manager, it goes. If this project ever genuinely moves to pnpm, the lockfile will be generated fresh — not borrowed from a file that had been stale for two weeks.
In the mirrored situation the script simply flips. An npm project carrying a leftover yarn.lock or bun.lockb from an old experiment hits the same wall, because Vercel applies the same detection to every package manager. And if pnpm does become the choice someday, the steps are just: regenerate the lockfile locally until it truly matches, then push. Never leave a stale lockfile in a repo that deploys every day.
What I take from this: one project, one lockfile, and the file that decides the deploy should be the file you actually maintain locally. Sounds obvious, but the incident proved otherwise. For another way the same deploy machine can bite, I wrote about the sitemap revert that collided with a Next 16 route.