How to Choose Between Content Hash and Version Hash for Static Assets

Selecting the wrong fingerprinting strategy causes stale asset delivery, 404 Not Found errors after deployment, and CDN purge budgets that spiral out of control. This guide maps concrete symptoms to root causes, provides a decision matrix for content hash vs version hash selection, and shows exact bundler configurations for both strategies.

The Core Distinction

Content hash derives the URL fingerprint directly from the bytes of a single file. Only files that change get new URLs. Assets that do not change keep the same URL and stay cached indefinitely at every CDN edge node.

Version hash (also called a build hash or release tag) derives the fingerprint from a release identifier — a package.json version string, a CI run ID, or a Git commit SHA. Every asset in the release shares the same prefix or suffix, even if 90% of the files are byte-identical to the previous release.

The tradeoff is between cache efficiency and operational simplicity.

Symptom-Based Diagnosis

Before choosing a strategy, check what is failing in your current deployment:

Symptom: Users see outdated JavaScript immediately after a deploy

Run this against your CDN origin to see what the edge is serving:

curl -sI -H 'Pragma: no-cache' https://cdn.yourdomain.com/assets/main.a1b2c3d4.js \
  | grep -iE 'cache-control|age|etag|x-cache'

An age header with a high value (e.g., age: 86400) means the CDN is not invalidating stale content. The root cause is usually one of:

  • The filename did not change (version hash not updated, or content hash not configured).
  • The CDN cache key ignores the filename path (e.g., normalises query strings but not paths).
  • The HTML entry point is also cached and still references old asset URLs.

Symptom: A CDN purge job is triggered for hundreds of files on every deploy, even though only two files changed

This is the classic failure mode of version hashing. The release version increments, every asset URL changes, and the CDN must re-fetch everything. The fix is content hashing.

Symptom: 404 errors for assets immediately after deploy

This indicates the HTML is referencing new hashed URLs that the CDN has not yet fetched from origin. See cache-key architecture for the atomic deploy sequence that prevents this race condition.

Decision Matrix

Factor Favour content hash Favour version hash
Deploy frequency Daily or continuous Weekly or monthly
Build determinism Strictly enforced Hard to guarantee
CDN purge cost Must minimise per-file purges Full-release purge is acceptable
Compliance/auditing Per-file change tracking Single-version audit trail
Rollback pattern Revert HTML manifest Route traffic to prior version directory
SRI enforcement Regenerate per build (automated) Stable within a minor update
Team size Large team, many PRs Small team, monolithic releases
Build output size Thousands of chunks Handful of bundled files

For most projects deploying at least weekly, content hashing is the better default. It is what Webpack, Vite, Rollup, and esbuild all produce out of the box.

Content hash vs version hash decision flow A flowchart starting with the question of whether the build is deterministic, branching to whether deploys are continuous, and ending at recommendations for content hash or version hash with a hybrid option for HTML entry points. Deterministic build? (same source → same hash) Yes No Fix determinism then revisit Deploys daily or more? or thousands of assets? Yes No Version hash per release Content hash per-file, immutable Hybrid: version hash for HTML, content hash for assets
Decision flow: start with build determinism, then deploy frequency, arriving at content hash, version hash, or a hybrid pattern for HTML entry points.

Content Hashing: Full Configuration

Content hashing is the default in every modern bundler. The key is ensuring you use the right token ([contenthash] not [hash] in Webpack) and that deterministic module IDs are enabled.

Webpack 5

// webpack.config.js
module.exports = {
  mode: 'production',
  output: {
    filename: '[name].[contenthash:8].js',
    chunkFilename: '[name].[contenthash:8].chunk.js',
    assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
    clean: true
  },
  optimization: {
    moduleIds: 'deterministic',
    chunkIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      chunks: 'all'
    }
  }
};

The runtimeChunk: 'single' setting isolates the Webpack runtime into its own small file. Without it, the runtime chunk (which contains module ID mappings) changes on every build, cascading hash changes into your vendor bundle even when vendor code is unchanged.

Vite

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: 'entry/[name]-[hash:8].js',
        chunkFileNames: 'chunks/[name]-[hash:8].js',
        assetFileNames: 'assets/[name]-[hash:8][extname]'
      }
    }
  }
});

For a complete walkthrough of Vite hash configuration options, see how to configure content hashing in Vite production builds.

CDN cache headers for content-hashed assets

# nginx — serve any path containing a hex fingerprint as immutable
location ~* -[a-f0-9]{8,16}\. {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header Vary Accept-Encoding;
}

The immutable directive tells the browser to not even send a revalidation request within the asset’s max-age window. Because the filename changes when content changes, there is no risk of serving stale content.

Version Hashing: Configuration and Use Cases

Version hashing is appropriate when build determinism is hard to guarantee, when you need instant rollback without touching CDN purge, or when compliance requires a single identifiable version identifier across all assets.

Nginx version-prefix routing

# nginx — map /assets/v<semver>/* to a release directory
location ~ ^/assets/v([0-9]+\.[0-9]+\.[0-9]+)/(.+)$ {
    alias /var/www/releases/v$1/static/$2;
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri =404;
}

Rollback is a one-line config change: update the version prefix in the HTML template and reload Nginx. No CDN purge required because the previous version’s URLs are still cached.

CloudFront cache policy for version-prefixed paths

{
  "CachePolicyConfig": {
    "Name": "Immutable-Versioned-Assets",
    "DefaultTTL": 31536000,
    "MaxTTL": 31536000,
    "MinTTL": 0,
    "ParametersInCacheKeyAndForwardedToOrigin": {
      "CookiesConfig": { "CookieBehavior": "none" },
      "HeadersConfig": { "HeaderBehavior": "none" },
      "QueryStringsConfig": { "QueryStringBehavior": "none" }
    }
  }
}

Strip cookies and query strings from cache keys. Only the URL path — which includes the version prefix — differentiates objects.

The Hybrid Pattern

The most pragmatic production setup combines both strategies:

  • HTML entry points (index.html, _document.html): served with Cache-Control: no-cache, must-revalidate so browsers always fetch the latest HTML. The HTML contains all the content-hashed asset URLs.
  • JavaScript and CSS bundles: content-hashed ([contenthash:8]), served with Cache-Control: public, max-age=31536000, immutable.
  • Vendor bundles: content-hashed — they change rarely and benefit most from long-term caching.
  • Critical CSS for above-the-fold rendering: optionally version-hashed to allow instant global rollback if a bad CSS deploy causes visual regressions.

This pattern is described in detail in cache-key architecture.

Verification After Switching Strategy

After changing your hashing strategy, verify that the CDN is serving the right content:

# 1. Check that the new fingerprinted URL returns a fresh response
curl -sI https://cdn.yourdomain.com/assets/main.NEW_HASH.js \
  | grep -iE 'cache-control|age|etag|x-cache'

# 2. Confirm the old URL returns 404 (content-hash strategy) or still serves the old file (version-hash)
curl -sI https://cdn.yourdomain.com/assets/main.OLD_HASH.js \
  | grep -i 'http/'

# 3. Confirm the HTML entry point has been updated to reference the new hash
curl -s https://yourdomain.com/ | grep -o 'main\.[a-f0-9]*\.js'

For strategies on what to do when you need to undo a deploy, see content hashing vs semantic versioning.

When to Reconsider Your Choice

Content hashing becomes problematic when:

  • The build is non-deterministic and you cannot fix it quickly — hashes change on every CI run, defeating the immutable-cache benefit.
  • Your CDN has a per-file purge rate limit and you need to purge thousands of assets simultaneously.
  • Your team operates entirely from release branches with long freeze periods — version hashing aligns better with that operational model.

Version hashing becomes problematic when:

  • Deploy frequency is high and the CDN purge cost scales with it.
  • Micro-frontend or module federation setups share assets across independently versioned apps — a shared asset’s version prefix is ambiguous.
  • You adopt SRI, because SRI requires per-file hashes that effectively give you content hashing at the integrity layer anyway.

Frequently Asked Questions

Can I use both content hashes and version hashes in the same deployment?

Yes. The hybrid pattern described above is common in production. Use version-based identifiers for HTML and version-sensitive entry points, and content hashes for everything those documents reference.

Does content hashing guarantee zero CDN purge costs?

No. The HTML entry points themselves are typically not fingerprinted and must be purged (or served with short TTLs) so browsers pick up updated asset URLs. The cache-key architecture guide covers how to structure this without a global purge.

How does SRI interact with version hashes?

SRI checks the byte content of the file, not the URL. If a version hash increments but the file bytes do not change, the SRI hash remains valid. If the file bytes change, the SRI attribute must be regenerated regardless of which URL strategy you use.

What happens to old version-hashed assets during a rollback?

Nothing — they are still cached at the CDN. You redirect traffic to the previous version prefix (by updating your HTML template or nginx config) and the CDN serves the old objects from cache without touching origin. This is the primary operational advantage of version hashing.