Troubleshooting guide

Deploy succeeded but production still serves the old site

The dashboard says the deploy succeeded, yet visitors still get an old build, a blank page, 404s on assets or functions that hang. This pattern appears in 156 verified builder reports in our data, and several say nobody noticed for days. In each case the pipeline confirms that its steps ran and does not check the production URL.

156 verified cases13 min read Updated 6 Nov 2026Last reviewed 7 Nov 2026How we collect cases

What you'll see

  • The deploy shows green or 'Ready', but the live site runs a build from weeks or months ago.
  • Pushes to the production branch stop triggering deploys while other branches still build, or deploy-hook jobs stay pending.
  • The build passes but production returns 404 for CSS, JS, login routes or serverless functions that work locally.
  • Some users see intermittent JavaScript chunk load errors or 'Failed to find Server Action' right after a release.
  • Every deploy fails with exit code 1 or 2 after one commit, or the CI deploy step returns an HTTP error while the change still goes live.
  • Usage tracking, audit writes or other background work silently stop after a release, with no error in the deploy log.

Why it happens

Cause 1

Tools, dependencies and images are unpinned

An install that resolves fresh versions on every build can pull a new major version of a dependency, a CLI or a base image. npm ci exists to prevent this: it installs from the lockfile, removes any existing node_modules first, and exits with an error if the lockfile and package.json disagree. A pipeline that runs npm install or npx some-cli with no version has no such guard.

Cause 2

Steps pass without anyone checking the result

In GitHub Actions, a run step with no shell set uses bash -e {0}, which exits on the first failing command but has no pipefail. A failure on the left side of a pipe can therefore pass. A script that ends with an unrelated successful command, or that swallows an error, exits 0 and the job is green.

Cause 3

Build output is replaced before the build is known to be good

A build step that deletes the output directory first, or a deploy that uploads only part of the output, leaves production with missing files. Platforms with atomic deploys avoid this. Netlify, for example, does not put changes on the public URL until all of them are uploaded, then switches the new version live at once. Hand-rolled copy or rsync steps usually have no such guarantee.

Cause 4

Code and its dependencies ship on separate paths

If the frontend deploys before the backend functions or the migration it needs, production breaks until the other half lands. During rolling releases, browsers run into the same problem: Next.js documents that version skew can produce missing assets, Server Function mismatches and failed navigations. Behind a load balancer, instances must also share one build and one NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, or you see 'Failed to find Server Action'.

Cause 5

The platform is no longer promoting new deploys

Some platform states stop new pushes from going live without reporting a failure. On Vercel, an Instant Rollback turns off auto-assignment of production domains, so later pushes to the production branch do not replace the rolled-back deployment until you promote one. On Netlify, a locked deploy stops auto-publishing to the production branch. Both show up as a stale site with green builds.

How to fix it

  1. Pin every tool, image and CLI to a version

    Commit the lockfile and install with npm ci, which fails on a mismatch instead of updating it. Pin the Node version in CI, pin deploy CLIs to a major version, and reference base images by digest, as the Dockerfile reference allows with <image>@<digest>.

    npm ci
    npx vercel@50 --version   # pin the CLI major you tested, not 'latest'
  2. Make every script fail loudly

    Set the shell explicitly so GitHub Actions uses bash --noprofile --norc -eo pipefail {0}, and start your own scripts with strict flags. Check the exit code of the deploy command itself, not the exit code of the last command in the script.

    set -euo pipefail   # at the top of every deploy script
    # in the workflow: defaults: { run: { shell: bash } }
  3. Build into a new directory and swap it in only on success

    Don't delete the live output until the new build succeeds. Build elsewhere, check that the output is non-empty, then rename. If your host does atomic deploys, use them instead of copying files into place by hand. The example uses Vite's --outDir.

    set -euo pipefail
    npm run build -- --outDir dist.new
    test -s dist.new/index.html
    rm -rf dist.prev
    [ -d dist ] && mv dist dist.prev
    mv dist.new dist
  4. Run migrations before the code that needs them, in the same pipeline

    Use the host's pre-deploy hook so a failed migration fails the whole deploy. On Render, the pre-deploy command runs after the build and before the deploy; if it fails or times out, the deploy fails and the service keeps running its last successful deploy. Write migrations so the old code still works against the new schema, and deploy backend functions before the frontend that calls them.

  5. Add a health check that the new version must pass

    Railway queries your health-check path until it gets a 2xx status and marks the deploy failed if that does not happen within the timeout (300 seconds by default). It runs only at deploy time, so it does not replace monitoring. Have the endpoint do real work, such as a database read, instead of returning 200 unconditionally.

  6. End every deploy by requesting the real production URL

    Publish a small version file at build time and have the last pipeline step fetch it from the public domain, as a visitor would. --fail-with-body makes curl exit with code 22 on a 400 or higher status and still print the body. Make that step fail the job and alert on it.

    curl --fail-with-body --silent --show-error --max-time 20 \
      https://example.com/version.json | grep -q "$GIT_SHA"
  7. Know your rollback and skew settings before you need them

    Vercel Instant Rollback points domains back to an earlier production deployment; you must promote a deployment afterwards to restore auto-assignment. Cloudflare Pages treats any successfully built production deployment as a rollback target. To stop chunk load errors during releases, enable Vercel Skew Protection or set Next.js deploymentId, so a client keeps talking to the deployment that served its page.

    // next.config.js
    module.exports = { deploymentId: process.env.GIT_HASH }

Check it's fixed

  • After a deploy, the version file on the public domain returns the new commit hash, fetched from outside your network.
  • Break the build on purpose in a branch: the pipeline goes red, an alert fires, and the previous production deployment keeps serving.
  • Open the site in a fresh browser profile and load a deep route, a static asset and one function-backed route; none return 404 or hang.
  • Re-run the pipeline on a clean runner and confirm the dependency, Node and CLI versions in the log match the pinned ones.

Specific fixes

Stuck on one exact error? These pages go deeper on one symptom each.

Fix it with Gemmein

On Gemmein, every publish first runs a browser test of the draft: an uncaught JavaScript error in the draft's own files, or a page that never finishes loading, stops it as FAILED test with nothing put up (if the test truly can't run, the publish goes ahead and says "Published UNTESTED"). Publishes land on the Development site, and a version reaches the Live site when your human approves a promote, which copies the same files rather than new ones.

  1. Test the draft as a customer sees it

    npx gemmein site test serves the draft privately and opens up to 12 pages at phone and desktop width. It answers one pass, warn or fail report, and each issue names its page and a fix, covering JavaScript errors, missing files, broken links and blank pages.

    npx gemmein site test
  2. Publish and read the answer

    Each publish answers PUBLISHED or FAILED, and a FAILED answer names the step, the file and line when known, the reason and the fix. If the test truly can't run, the publish goes ahead and says "Published UNTESTED — <why>".

    npm run build
    npx gemmein publish
  3. Promote the data model, then put the site version live

    After go-live, new collections and field changes reach Live by promotion, and the docs say to promote first, then deploy the frontend that depends on them. Once the app is live, npx gemmein site promote <version> asks your human to approve putting one Development version on the Live site; before go-live it is refused (not_live).

    npx gemmein go-live
    npx gemmein site promote <version>
  4. Read what the Live site serves and what its pages hit

    site_status reads both addresses and the version each serves. npx gemmein site errors --live lists what real visitors' pages reported, including uncaught errors, refused Gemmein calls and files that didn't load (404 when the version has no such file); it needs the live-read grant your human approves in the browser.

    npx gemmein login --live-read
    npx gemmein site errors --live

Questions

Why is my deploy green when the site is still old?

Either the platform is not promoting new deploys (a rollback or a locked deploy), or the pipeline only checks that its steps ran. Check which deployment is assigned to the production domain, then add a post-deploy request to the real URL.


Why do users get chunk load errors right after a release?

Their open page references JavaScript files from the previous build, and the new deployment no longer serves them. Version-skew protection, such as Vercel Skew Protection or Next.js deploymentId, keeps client requests on the deployment that served the page.


Is npm ci enough to pin dependencies?

It pins what is in the lockfile and fails if the lockfile and package.json disagree. You still need to pin the Node version, deploy CLIs and base images, which the lockfile does not cover.


Should migrations run before or after the new code?

Before, in the same pipeline, so a failed migration stops the release. Write them so the old code still works against the new schema, because old and new instances run side by side during a rolling deploy.


Sources

Last reviewed 7 Nov 2026. What changed: No changes: steps re-checked against 8 official page(s). Checked against: vercel.com, vercel.com, docs.netlify.com, render.com, docs.github.com, docs.npmjs.com, nextjs.org, docs.railway.com. How we check this

← Back to the full report