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
- 9 min
On this page
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 page | Hash moved |
|---|---|
| A visible word in the home page's heading | Yes |
| The home page's social card image URL | Yes |
| A note's publication date, changed only in its JSON-LD | No |
An image swapped behind next/image, same alt text and size | No |
| A stylesheet's URL, which is all a CSS-only deploy changes | No |
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
- Next.js — generateBuildId
- Next.js — deploymentId
- Next.js v16.1.6 source — output-export-prefetch-encoding.ts (the build id comment)
- Next.js v16.1.6 source — generate-build-id.ts
- Next.js Discussion #53346 — Can I set non-changing build id in next.config.js?
- Next.js 13 — next.config.js Options: generateBuildId
- nexdrew/next-build-id
- next-shared-cache — Build ID as prefix key
- IndexNow — Documentation
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 →
Related
AI automation for small business: what stays human
The lead, email and content automations Easinvy runs for small businesses on n8n and code, and the points where a person still reviews, takes over or checks.
Custom ERP vs off-the-shelf ERP, from one we run
An incense maker's custom ERP, read from its code: cartons that mix fragrances, stock that cannot go below zero, and what Tally and Odoo already do.
Web Application Development
Applications built to still make sense in year three.
All notes
Write-ups from building and running our own product.