# How do I set up CDN asset versioning: hashing and a cache strategy on deploy?

> Drop query-string busting: cache content-hashed filenames as `immutable`, keep the HTML short-lived, and upload the assets before you cut over.

- Asked: 2026-05-24
- Answered: 2026-05-27
- Asked by: Aslı
- Tags: performans, ci-cd, altyapi
- Source: https://www.muhammetsafak.com.tr/en/just-ask/cdn-asset-versioning-and-cache-strategy-on-deploy/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** When I push new code live with DeployerPHP, the UI breaks because of stale CSS/JS in the user's browser. Some CDNs ignore a query string like `mix.js?id=123` and keep serving the old version.

For a permanent fix I want to hash the asset names (`app.a8f9b2.js`) and ship them with `Cache-Control: max-age=31536000` to the CDN. How do I design the build and invalidate automation in CI/CD?


Short answer: query-string cache busting is unreliable; the durable fix is content-hashed filenames (`app.a8f9b2.js`) — the name changes only when the content changes, and the cache strategy handles the rest.

The root of your problem: with `app.js?id=123` the filename is always the same. Some CDNs strip or ignore the query string in the cache key, so even when `id` changes they keep serving the OLD cached file. You need to change the cache key — the name, not the query.

1. **Use content-hashed filenames.** Like `app.a8f9b2.js`, where the hash is derived from the file's contents. Change the content and the name changes; leave it unchanged and the name stays. That lets you cache every asset forever with `Cache-Control: public, max-age=31536000, immutable` — the `immutable` directive tells the browser "don't even revalidate this," cutting needless conditional requests.
2. **The one file you must NOT cache long: the HTML/entrypoint.** Make the HTML that references the hashed assets (or the entrypoint that references the manifest) short-lived or `no-cache`. The logic is simple: the HTML must always be fresh so it points to the NEW hashes; the assets already carry the right version via the hash. Cache the HTML long and the whole chain locks up.
3. **CI/CD: upload first, then go live.** Build hashed output with the Vite manifest. The order is critical: upload the assets to the CDN/bucket BEFORE flipping the app live. Otherwise the new HTML points to a hash that isn't there yet and you get 404s. In a DeployerPHP flow, wire this as an "upload before switching the symlink" step.
4. **Keep the previous release's assets around for a while.** At deploy time, a user with the page open is still running the old HTML and requesting the old hashes. If you don't delete the previous release's assets, these in-flight users don't break. DeployerPHP's `keep_releases` already gives you exactly this.

**Bottom line:** I'd set up the trio of content-hashed `immutable`-cached assets + short-lived HTML + upload-before-cutover. With true content hashing you practically never need to purge assets — since the name changes, the old file just sits there untouched and nobody requests it. The only thing you ever invalidate is the HTML, and that's already short-cached. Drop query-string busting entirely.

## Related Reading

- [How do I set up secretless AWS access in CI/CD with OIDC and IAM roles?](https://www.muhammetsafak.com.tr/en/just-ask/secretless-ci-cd-with-aws-iam-roles-and-oidc/) — Just Ask
- [How do I shrink Docker images with multi-stage builds and distroless?](https://www.muhammetsafak.com.tr/en/just-ask/shrinking-docker-images-with-multi-stage-builds-and-distroless/) — Just Ask
- [How do I safely manage Terraform state with S3 + DynamoDB locking?](https://www.muhammetsafak.com.tr/en/just-ask/terraform-remote-state-and-locking-with-s3-and-dynamodb/) — Just Ask
