Skew protection
Old clients keep fetching their own build's chunks across rollouts and rollbacks.
Version skew happens when more than one revision serves at once — during a rollout, or when a
rollback canary splits traffic. A browser that loaded build A keeps requesting
_next/static/A/... chunks even after the server rolls forward to build B. If A's assets are
gone, that client breaks. With object storage configured, knext keeps old clients working by
serving every build's assets from the durable object store and only reaping them under a
build-id-aware retention rule. Without a storage block the deploy-pinned build id and the
?dpl= client pinning below still apply, but the asset-retention half does not — see
Without object storage.
Stable, deploy-pinned BUILD_ID
knext ties Next.js's BUILD_ID to the deploy tag rather than a random id. At build time the CLI
sets NEXT_DEPLOYMENT_ID to the deploy tag, and next.config reads it back as the build id:
generateBuildId: () => process.env.NEXT_DEPLOYMENT_ID || null,Scaffolded apps ship that line already. Returning null keeps a plain local build on its own
generated id, so building outside kn-next deploy is unchanged. Use || rather than ?? — with
??, an environment that exports NEXT_DEPLOYMENT_ID as an empty string produces an empty build
id rather than falling back.
Because the build id is the deploy tag, the uploaded asset prefix (_next/static/<id>/), the
image tag, and the prefix the GC prunes by all share one value. NEXT_DEPLOYMENT_ID also makes
the build append ?dpl=<id> to asset and RSC requests — client-to-build pinning, so a client only
ever resolves chunks for its own build — and the deploy carries the same id into the running pod's
environment.
If the deploy tag's asset prefix is not there after the build, kn-next deploy aborts before
anything is uploaded or applied. It never warns and continues. The two ways to see this are a
next.config.ts with no generateBuildId, and --skip-build run against a build directory left
over from an earlier tag — in that second case the assets would otherwise be uploaded under the old
build's prefix, which the collector may then reap while the new revision is serving. The check runs
only for deploys that upload assets: with no storage block, or with --skip-upload, there are no
remote prefixes to keep in step and nothing to check.
Adding this to an app you already have
An app created before this existed has no generateBuildId, so its first deploy after upgrading
stops with the abort above. Add the one line, and nothing else changes:
const nextConfig: NextConfig = {
generateBuildId: () => process.env.NEXT_DEPLOYMENT_ID || null,
// ...the rest of your config
};Local next dev and next build are unaffected — outside kn-next deploy the variable is unset
and the fallback keeps Next's own id.
Uploads are additive
A deploy uploads the new build's assets but never prunes on upload — old builds' static prefixes persist. Pruning is a separate, bounded step, so a fresh deploy can never strand a client that is mid-session on the previous build.
Build-id-aware GC
After upload, knext reaps old static prefixes under two keep rules, OR'd together:
- Retain window — keep the newest N build-ids. N is
storage.assetRetention(default 3). - Live set — keep any build-id currently serving traffic, even if it is older than the window.
The live set is read read-only from NextApp.status.currentTraffic: knext parses each live
revisionName, then resolves it to a build-id via a label the operator stamps onto the revision.
So a pinned, canaried, or rolled-back build is never reaped while it serves — even if it has
aged out of the retain window.
Deletes are scoped strictly to <app>/_next/static/<buildId>/; the bare <app>/ prefix is
teardown-only and is never a prune target. Shared, content-hashed directories (chunks/, css/,
media/) are never treated as build-id prefixes, so assets referenced across builds are never
candidates for pruning.
The GC runs automatically after every deploy (best-effort — a GC failure never fails a deploy that
has already shipped) and is also available as a standalone command, kn-next gc, for scheduled or
manual runs — see the CLI reference.
The GC is fail-safe: if any live revision cannot be resolved to a build-id (label missing or the read failed), the whole GC is skipped for that deploy. The only failure mode is over-keeping assets — never over-deleting a build a client still needs.
Each upload also writes a small marker object into its own _next/static/<id>/ prefix, and the
collector only ever considers prefixes that carry one. Anything else in that space — an upload from
an older knext, a directory a future Next release introduces, a file someone put there by hand — is
kept. That is the same direction as the rule above: a prefix whose origin cannot be established is
kept forever rather than guessed at.
Tuning retention
storage: {
provider: 'gcs',
bucket: 'acme-assets',
assetRetention: 5, // keep the newest 5 build-ids (default 3)
},The live set is added to whatever the window keeps, so raising assetRetention widens the skew
window for normal rollouts; live revisions are protected regardless.
The browser-level proof — a real client on a canaried old build fetching its own chunks — runs as a nightly end-to-end test, not on every PR.
Without object storage
storage is optional. Without it, each revision serves its own build's assets from its container
image, and this page's guarantees split in two:
- Navigations stay protected. The deploy-pinned
BUILD_IDand?dpl=client pinning are independent of storage, so a client on an old build still detects the skew and reloads onto the new one. - In-flight fetches lose their safety net. Once an old revision is fully retired and scaled
away, a chunk request that was already in flight from a client still on that build has no object
store to fall back to and can fail. The durable store is exactly what covers that window, and it
is the main reason to add a
storageblock as your traffic grows. - Rollback gets safer. A pinned revision's assets live in its own image, so no retention rule can ever reap them.
Every storage-less deploy announces this mode explicitly, and kn-next gc reports there is
nothing to reap. See Starting without object
storage for the full trade-off and the growth
path.
Related
- Rollback & traffic split — the traffic split that puts an old build back in the live set.
- Multi-cloud deploy — where per-build assets are stored and served from.
- Operator & the NextApp CRD — the
buildIdspec field andcurrentTrafficstatus field.