# Cloudflare Build Failed, Site Not Updating: 90 Minutes

> When a build fails, the old site keeps serving with no error. On August 11, 2026, one lockfile change did that to this site for 90 minutes.

Canonical: https://brandonlazovic.dev/articles/deploy-failed-site-looked-fine/  
Author: Brandon Lazovic  
Published: 2026-10-08

## The short version

- On a static site, a fix you push is invisible to Google until a build succeeds and deploys, and a failed build leaves the previous version serving with no error anywhere.
- On August 11, 2026, one package install on a Windows machine removed two entries from this site's lockfile (the file that pins every package version), and every push failed to build for 90 minutes while the live pages never changed.
- The check that should have caught the broken lockfile, the standard dry-run install check (npm ci --dry-run), reported 'up to date' on it, so only the real Linux build exposed the fault.
- A push that produces no deployment newer than your commit is a failed build once about five minutes pass, and the newest deployment is the last entry in wrangler's list, not the first.

A static site is a set of finished pages that a build step produces from source files. The live site changes only when a build succeeds and its result is deployed, meaning published to the servers that answer visitors and crawlers. When the build fails, nothing announces it. The previous build keeps serving, so the site looks healthy while every fix you push (send to the code repository) sits unpublished. On August 11, 2026, one package install on my Windows machine broke every build of this site for <span class="hand-ring">90 minutes</span>, and for search that means a fix Google cannot see until it deploys. [1]

> On August 11, 2026, every push to brandonlazovic.dev failed to build for 90 minutes, and the site kept serving the previous build the whole time.

## Why does a failed build leave the old site live?

On this site, a failed build leaves the old site live because a deploy replaces the live pages only after a build finishes. The build turns source files into finished pages, and the host swaps them in when that succeeds. If the build fails, nothing gets swapped, so visitors and crawlers keep receiving the previous version with no error page and no change in status code.

This site's own incident record describes exactly that. In its words, "The site kept serving the previous build, so nothing looked broken from outside; the only visible symptom was that no new deployment appeared." [1] Cloudflare rebuilds this site on every push, so a push is a publish, and a push that never deploys is a fix that never went out. [2]

This site runs on Cloudflare as a Worker (a small program) that serves static files, and I have first-hand evidence only from that setup. The symptom, a failed build and an unchanged live site, is the one the search phrase "cloudflare pages build failed site not updating" describes, but I did not test a Pages project.

## What happened during the 90 minutes of failed builds?

On August 11, 2026, one command, `npm install -D playwright` (npm installs a project's add-on code packages, and Playwright is a browser-testing package), removed two entries from the file that pins every package version (the lockfile), and every push failed to build for 90 minutes. Because the lockfile fixes the exact version of every package, each build installs an identical set. With two entries missing, Cloudflare's installer refused the file. [1]

The two missing entries were small helper packages that a few other packages need on some platforms. For anyone searching the error, they were `@emnapi/core` and `@emnapi/runtime`. Cloudflare's build stopped with an error of this shape: `npm error Missing: @emnapi/core@1.11.1 from lock file`. [1]

The npm documentation for `npm ci`, the installer that builds run, sets a strict rule. The command "must have an existing `package-lock.json`". It also says that "If dependencies in the package lock do not match those in `package.json`, `npm ci` will exit with an error, instead of updating the package lock." [4]

The timeline below shows the window. The amber stretch is the span where every build failed and the live site did not change.

<figure class="deploy-timeline" style="margin:2rem auto;max-width:460px"><svg viewBox="0 0 420 372" role="img" aria-labelledby="dt-title dt-desc" data-sketch style="display:block;width:100%;height:auto;font-family:var(--sans)"><title id="dt-title">The 90-minute failed-build window on August 11, 2026</title><desc id="dt-desc">A vertical timeline. First, an install on Windows drops two entries from the lockfile. Next, the first push fails on Cloudflare's Linux build. For 90 minutes every push fails and the live site keeps serving the previous build. Then the dropped entries are restored from the last good commit. Finally builds pass and a new deployment appears.</desc><line x1="28" y1="34" x2="28" y2="120" style="stroke:var(--stone);stroke-width:2"/><line x1="28" y1="120" x2="28" y2="252" style="stroke:var(--amber);stroke-width:4"/><line x1="28" y1="252" x2="28" y2="338" style="stroke:var(--stone);stroke-width:2"/><circle cx="28" cy="40" r="7" style="fill:var(--indigo)"/><circle cx="28" cy="120" r="7" style="fill:var(--indigo)"/><circle cx="28" cy="252" r="7" style="fill:var(--amber)"/><circle cx="28" cy="338" r="7" style="fill:var(--indigo)"/><line x1="47" y1="189" x2="131" y2="187" style="stroke:var(--amber);stroke-width:2.4"/><polyline points="254,196 262,198 262,224 254,226" style="stroke:var(--amber);stroke-width:2.4;fill:none"/><text x="48" y="38" style="fill:var(--ink);font-size:15px;font-weight:700">One install on Windows</text><text x="48" y="58" style="fill:var(--ink-soft);font-size:13px">Two entries drop out of the lockfile</text><text x="48" y="118" style="fill:var(--ink);font-size:15px;font-weight:700">The next push fails to build</text><text x="48" y="138" style="fill:var(--ink-soft);font-size:13px">Cloudflare's Linux installer refuses the file</text><text x="48" y="182" style="fill:var(--amber);font-size:15px;font-weight:700">90 minutes</text><text x="48" y="202" style="fill:var(--ink-soft);font-size:13px">Every push fails. The live site</text><text x="48" y="220" style="fill:var(--ink-soft);font-size:13px">keeps serving the previous build.</text><text x="48" y="250" style="fill:var(--ink);font-size:15px;font-weight:700">Entries restored from the last good commit</text><text x="48" y="270" style="fill:var(--ink-soft);font-size:13px">Copied back verbatim, not regenerated</text><text x="48" y="336" style="fill:var(--ink);font-size:15px;font-weight:700">Builds pass</text><text x="48" y="356" style="fill:var(--ink-soft);font-size:13px">A new deployment appears</text></svg><figcaption style="margin-top:.75rem;font-size:.875rem;color:var(--stone)">The August 11, 2026 incident in order, not to scale. The amber stretch is the 90 minutes when no push could publish, from this site's own record. [1]</figcaption></figure>

## Why did the install work on my machine and fail on Cloudflare's?

The install worked locally because my machine and Cloudflare's build server run different software. Cloudflare builds with npm 10.9.2 on Linux, and this machine runs npm 11.6.2 on Windows. The newer npm on Windows cut out entries that do not apply to Windows and left in place the packages that depend on them. Linux then read a lockfile pointing at entries that were gone. [1]

The site's record puts the mechanism this way: "The Windows resolver PRUNES platform-inapplicable transitive deps from package-lock.json while leaving the parents that reference them." [1] A transitive dependency is a package that another package needs, and pruning means cutting it out.

Nothing on the Windows side complained. The lockfile was wrong for Linux and looked fine everywhere I could see it, which is why the first sign of trouble was a deployment that never arrived.

## Why didn't npm ci --dry-run catch the broken lockfile?

The usual check, `npm ci --dry-run`, reported "up to date" on the broken lockfile, so it caught nothing. The command `npm ci` is what Cloudflare's build runs to install exactly what the lockfile lists, and the dry-run flag simulates it without installing. With the folder of already-installed packages present, the simulation finished in about a second without comparing anything. [1]

I ran it specifically to rule this out, and it gave a clean answer on a file that fails the real command. Re-running `npm install --package-lock-only`, the command that rewrites only the lockfile, does not help either, because it prunes again. [1]

The repair was a restore. I took the dropped entries verbatim from the last lockfile that built, so no integrity hash was invented. An integrity hash is the fingerprint an installer uses to confirm it received the right file, and copying entries back means none of those fingerprints had to be made up. [1]

This site's publish gate (the checks I run before publishing) now starts with a lockfile check that needs no built pages, so a fault like this one fails in milliseconds instead of after a build. The [redirect headers piece](/articles/check-redirect-headers/) lists it as the first of the gate's scripts.

## What does a stuck deploy cost you in search?

A stuck deploy costs you every fix and every page that depends on a build to appear. Google reads only what the live site serves, and the live site serves the last successful build. A corrected title, a redirect, a schema fix or a new article all stay unpublished until a build passes, and nothing in the site's own pages signals the gap.

Scheduled articles show the cost most clearly. On this site, a future `pubDate`, the publish-date field in an article's header, removes the page from the built site entirely, so a scheduled article is absent until a build runs on or after its date. [3] An automated GitHub job named `article-reveal.yml` runs at 05:00 Coordinated Universal Time (UTC) and reveals any article whose date is today, and the article still needs a working build to become a page. [3]

My read, suspected and not observed, is that a reveal landing inside a failing window would publish nothing. No scheduled article fell inside the 90 minutes, so this is the mechanism followed to its end and not an incident.

The site's own pre-publish check can't see any of this either. That check, `npm run audit`, reads files on disk, and a pass on a local build says nothing about whether Cloudflare's build passed on the same push.

## How do you tell a failed deploy from a slow one?

Compare the time of your latest commit with the time of the newest deployment. If a deployment newer than the commit exists, the push published. If the newest deployment is older than the commit and about five minutes have passed, the build failed.

By the script's own note a build takes a minute or two, and one recorded run on September 29, 2026 deployed 293 seconds after its commit, so treat five minutes as the minimum wait before calling a build failed. [2]

To run the check by hand on a Workers project, follow these steps.

1. Read the commit time with `git log -1 --format=%cI`.
2. List the deployments with `npx wrangler deployments list --name <your-worker-name>`, using Cloudflare's command-line tool, wrangler.
3. Find the newest `Created:` line that starts at the left margin. The list prints oldest first, so the newest is the last one, and the indented `Created:` lines under `Version(s):` belong to versions, not deployments.
4. If that time is later than your commit, you published. If it is earlier and the five minutes have passed, open the Cloudflare build log.

Step 3 is where I got it wrong first. On September 18, 2026, a check I wrote by hand read the first entry, compared a days-old timestamp against the push time, and reported a failed build on a deploy that had succeeded 44 seconds after the push. [2] A wrong alarm on every healthy deploy is worse than no check, because the false failures teach you to ignore the real one.

The durable fix is `scripts/deploy-check.mjs`, run with `npm run deploy-check`. It reports one of four states: the push published, the build failed, no deployment data came back because the list was empty, or the check is still pending because the five-minute window has not closed, which is neither a pass nor a failure.

The script's tests include a copy of the buggy reader and confirm it gives the wrong answer on the same input, so they would have failed on my original mistake. [2]

Treat "pushed" as unfinished work on any static site until a deployment newer than your commit exists. Every SEO fix ends at the deploy, and a green local check cannot tell you whether it happened.

<aside class="guardrail"><span class="lab">Accuracy guardrail</span> This is one site and one incident, on a Cloudflare Worker serving static files, and I did not test a Pages project. The 90 minutes and the two dropped entries come from this site's own record of August 11, 2026. The scheduled-article consequence is the mechanism read from how the site builds pages, and no reveal fell inside the window.</aside>

## Sources

1. First-hand: brandonlazovic.dev deploy incident of August 11, 2026, as recorded in this site's CLAUDE.md (Deploy trap section)
2. First-hand: this site's scripts/deploy-check.mjs and scripts/deploy-check.test.mjs, written September 18, 2026
3. First-hand: this site's CLAUDE.md notes on scheduled articles and the article-reveal workflow
4. npm documentation: npm ci (lockfile requirement) — https://docs.npmjs.com/cli/v10/commands/npm-ci
