Skip to content

Web Application Development

The Next.js build id changes every build

Two builds of one Next.js commit differed on all 36 pages, and only by the build id. Where Next writes it, and how easinvy.com's build hashes around it.

Ridam Agrawal

Founder, Easinvy

Published
Read
9 min
On this page

In short

Why does the Next.js build id change on every build?

Next.js gives every build a new random id unless next.config sets generateBuildId, and uses it to tell builds apart. On easinvy.com, a Next.js 16.1.6 site, the id sits twice in every prerendered page: in a comment after the doctype and in the inline hydration data. Stripping both made two builds of one commit identical.

Everything below was measured on easinvy.com, the site this note is published on, on 31 October 2026. The site is built with Next.js 16.1.6 and Turbopack and deployed on Vercel, and every production build ends by working out which of its pages changed since the last deploy. Next.js makes that harder than it sounds, because a new build id lands in every page on every build.

Why the Next.js build id changes every build

The Next.js build id is a name for one run of next build, written to .next/BUILD_ID. Next.js 16.1.6 uses the id that generateBuildId in next.config returns, when there is one. When there is none, its build code draws a random 21-character id with nanoid, and draws again if the id contains "ad", so that ad blockers do not mistake it for an ad. easinvy.com sets no generateBuildId, and two builds of the same commit on 31 October 2026 got two different ids.

The id is meant to change. The generateBuildId documentation says Next.js generates it "to identify which version of your application is being served". Next.js's own source shows one place that depends on it: when the browser prefetches a page, the client "performs a range request to get the build id" from the start of that page's HTML. "The first 64 bytes of the document are requested." An id that is not the client's own means the page belongs to another build.

So the Next.js build id changing on every build is not a bug. It is how Next.js tells two builds apart, and a fix that stops it changing takes that away.

Where Next.js writes the build id into a page

The Next.js build id appears twice in every page easinvy.com prerenders. Two builds of the same commit were compared page by page on 31 October 2026. All 36 prerendered pages differed, and with the build id replaced by a placeholder, all 36 were identical. Nothing else in them had changed.

The first copy is a comment straight after the doctype, as in one of that day's builds: <!DOCTYPE html><!--9TSJtP0Fs_TkznneQlY3_-->. The second is in the inline hydration data, a self.__next_f.push script, as the value of a "b" key.

The two copies are not always the same string. That build's id was 9TSJtP0Fs_TkznneQlY3-, ending in a hyphen, and its comment ends in an underscore. The source gives the reason: "Replace hyphens with underscores so it doesn't break the HTML comment." Search a page for the id in .next/BUILD_ID, and whenever the id has a hyphen, only one of the two copies turns up.

The same source file opens with "In output: export mode, the build id is added", but the code that calls it does so for every page prerendered at build time. easinvy.com is not a static export, and every one of its prerendered pages carries the comment, on the live site as well.

The inline scripts that hold the second copy are also why easinvy.com's Content Security Policy keeps 'unsafe-inline'.

Pinning the build id does not say which pages changed

Pinning the Next.js build id is the usual advice when it changes on every build, and it solves a different problem. The Next.js 13 documentation named it: "problems in multi-server deployments when next build is run on every server". Servers that each build the same code must agree on one id, so generateBuildId returns something they share, usually the git commit hash, and the next-build-id package does exactly that. The other way out is to build once and, as one cache handler's guide puts it, "use the same build to deploy to all environments".

A commit hash makes two builds of the same commit agree. It does nothing for a deploy, because a deploy that changes anything is a new commit, and the new hash lands in every page again.

A constant id goes further. Next.js discussion #53346 was opened on 30 July 2023 by someone hoping "if I had a non-changing build id, this issue wouldn't occur again". Six weeks later the asker wrote: "Tried it. Terrible idea". Two weeks after that came a report of the site opening on a white 500 page, and "Don't know why". Another reply: "Having a non-changing build ID is a wrong approach, I think." On 31 October 2026 the discussion was still marked Unanswered.

deploymentId is the newer route. From Next.js 16.2, "the build ID is constant when deploymentId is set", and the same page says Next.js "Injects a data-dpl-id attribute" on the <html> element: a per-deployment value in every page instead. easinvy.com runs 16.1.6 with no deployment id, so that was not tested.

Hash what the reader sees

easinvy.com's build has compared its pages between deploys since 15 September 2026, and the first attempt ran straight into the build id: all nineteen pages the site had then hashed differently on every build, so the comparison could never single anything out. The commit that fixed it records how the cause was found: "by building twice and diffing the stripped HTML, not by reading docs."

The fix is to hash only what a reader sees. The last step of easinvy.com's build removes the parts of each prerendered page that Next.js changes on every build, then hashes what is left:

function contentOf(html) {
  return html
    .replace(/<script\b[\s\S]*?<\/script>/gi, "")
    .replace(/<!--[\s\S]*?-->/g, "")
    .replace(/\/_next\/[^"'\s)]*/g, "")
    .replace(/\r\n/g, "\n")
}

Script elements go, and the hydration data with its copy of the id goes with them. Comments go, which takes the other copy. /_next/ asset URLs go, and Windows line endings become Unix ones. Each page's SHA-256, cut to 16 characters, goes into a manifest of URL to hash that deploys with the site, at https://easinvy.com/indexnow-manifest.json. The next production build fetches the manifest the live site is serving and compares, so no state is kept in the repository. The URLs whose hash changed are sent to IndexNow, which asks for "a URL of your website which has been added, updated, or deleted".

Measured on 31 October 2026, two builds of one commit gave byte-identical manifests of 33 pages; the other three prerendered pages, for passwords and email verification, are marked noindex and left out. Removing comments alone left all 36 prerendered pages different, and so did removing scripts alone. Only both together brought the difference to zero. The same day, all 33 pages easinvy.com served hashed to exactly their entry in the live manifest.

One edited title changed five pages

Hashing a Next.js build's output finds pages that a look at the source would miss. Two more builds of easinvy.com on 31 October 2026 each made one edit. Changing one heading inside one note moved exactly one hash, that note's. Changing one note's title moved five: the note itself, the two other notes that list it under related reading, the /notes listing, and the service page that lists its notes.

A diff of the commit would show one changed file in both cases. The hashes show every page a reader would find different, including the four that only display the title somewhere. For anything that acts on changed pages, that is the list that matters, and it is a list the source cannot give without building the site.

What the hash cannot see

The hash easinvy.com builds is blind to whatever it strips, and that is more than the build id. Each of these edits was made to a built page on 31 October 2026, and the page was hashed again:

Edit to the built pageHash moved
A visible word in the home page's headingYes
The home page's social card image URLYes
A note's publication date, changed only in its JSON-LDNo
An image swapped behind next/image, same alt text and sizeNo
A stylesheet's URL, which is all a CSS-only deploy changesNo

JSON-LD sits in a script element, so it goes with the hydration data. Images served through next/image have /_next/image URLs, so they go with the assets. A deploy that changes only styles or scripts moves no hash at all. For a list of pages whose words changed, that is the intent. For anything that has to know about structured data or images, it is a gap.

The /_next/ rule was not needed for the build id itself. Between two builds of the same commit the asset URLs were identical: the chunk file names are hex strings that carry no build id. The rule matters for deploys that do change them.

Line endings changed every page through one icon

Line endings broke easinvy.com's page comparison on 31 October 2026, in a place the hash's own normalising cannot reach. A build made from a fresh Windows clone of easinvy.com was compared with the 33 pages production serves, both from the same commit. All 33 differed. With one value set aside, the hash in the icon URL /icon.svg?icon.dbc8d6ca.svg, which production writes as d4c6e4f8, 32 of the 33 matched. The 33rd, the pricing page, reads its plans from the product's backend, and that clone had no address for one.

Git had checked app/icon.svg out with Windows line endings in that clone, and Next.js names the icon by a hash of its bytes. The script turns Windows line endings into Unix ones in the HTML, but the icon's hash was computed from the file before any HTML existed. Production builds on Linux and compares with Linux, so it is not affected. A comparison run from a checkout like that clone would report every page as changed.

Two flaws this note found in the script

Writing this note turned up two flaws in easinvy.com's own script, and both were fixed on 31 October 2026.

The home page could not be found. Since 30 October 2026 the sitemap had listed it as https://easinvy.com, with no trailing slash, to match its canonical address, while the manifest still keyed it https://easinvy.com/. The comparison looks each sitemap URL up in the new manifest and the old one. For the home page both lookups found nothing, and nothing equals nothing, so a change to the home page alone would have sent no URLs. The manifest now keys every page the way the sitemap writes it:

function urlOf(route) {
  return route === "/" ? `https://${HOST}` : `https://${HOST}${route}`
}

The general point: the key a diff looks things up by has to be the same string the list uses, character for character.

The script's own comment also named the wrong place for the build id. It said Next.js stamps the id into "every page's script URLs". No script URL carried it in these builds. The comment now names the two places this note measured: the comment after the doctype and the hydration data.

What this does not check

Everything in this note comes from one site: easinvy.com, Next.js 16.1.6 with the App Router, built with Turbopack, on 31 October 2026. Webpack builds, the Pages Router, static exports and other versions of Next.js were not tried. The copy of the id in the hydration data was seen in every page but not traced in the Next.js source.

A deployment id, and Vercel's skew protection, which uses one, were not tested. Going by the documentation, the data-dpl-id attribute it adds would need stripping too.

The build logs that would show what each production deploy sent were not read, so nothing here says which URLs a given deploy submitted, or what happened to them afterwards.

This is the build of the site you are reading. Knowing what a Next.js build actually changes, page by page, is part of web application development.

Questions people ask

Is it safe to set a constant build id in Next.js?
Not as a way around a changing id. Next.js uses the build id to tell one build from another, and the asker of Next.js GitHub discussion #53346 tried a constant and called it a terrible idea. From Next.js 16.2, setting deploymentId makes the build id constant, because version skew is then detected from the deployment id.
Should the Next.js build id be the git commit hash?
When the same commit is built on several servers or for several environments and the builds must agree, yes: generateBuildId can return the commit hash, and the next-build-id package does exactly that. A commit hash still changes with every commit, so it cannot show which pages changed between two deploys.
What does deploymentId do in Next.js?
deploymentId names a deployment so that Next.js can detect version skew. The Next.js documentation says it appends ?dpl= to static asset URLs, adds deployment headers to client navigations and a data-dpl-id attribute to the html element, and from version 16.2 makes the build id constant. easinvy.com does not set it.
How do you find which Next.js pages changed between two builds?
Hash each prerendered page after removing what Next.js changes on every build, then compare the hashes with the previous deploy's. easinvy.com's build strips script elements, HTML comments and /_next/ URLs before hashing. On 31 October 2026, two builds of one commit gave identical hashes, and one edited heading moved exactly one.

Sources

Ridam Agrawal

Founder and engineer at Easinvy. Writes up the parts of building and running a multi-tenant product that were expensive to get wrong. More about the studio →

Keep reading