knext

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:

next.config.ts
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:

next.config.ts
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:

  1. Retain window — keep the newest N build-ids. N is storage.assetRetention (default 3).
  2. 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

kn-next.config.ts
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_ID and ?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 storage block 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.

On this page