Back to blog
Secure SaaS Deployment9 min read

Static Deployments Can Fail Quietly When HTML Goes Stale

Hashed assets help with cache safety, but static sites can still serve stale behavior when old HTML keeps referencing old scripts, images, fonts, or service-worker code.

Static SitesCloudFrontSecure DeploymentCache Control
Static Deployments Can Fail Quietly When HTML Goes Stale article preview image

Static sites still have deployment risk

Static sites feel simple because the runtime shape is simple. Build the application, upload the files, let a CDN serve them, and avoid running a server for every request. For marketing sites, documentation, blogs, and many product-facing surfaces, that simplicity is a strength.

But static does not mean risk-free. Once a static site uses a CDN, hashed assets, route rewrites, service workers, generated images, and multiple deployment steps, it becomes a small delivery system. That delivery system can fail quietly.

One of the easiest failures to miss is stale HTML. A team may successfully build new JavaScript, new CSS, new images, and new generated assets, while an old route document remains live. The browser then keeps asking for old files because the old HTML is still telling it to.

That can look like a browser cache problem. It can look like a CloudFront invalidation problem. It can look like an image-generation problem. Sometimes the real issue is simpler and more frustrating: the live page is not the page the team thinks it deployed.

Hashed assets are only half the story

Modern frontend builds often generate hashed file names. A Vite build might produce asset names such as index-B6M_cqKU.js or image variants with content fingerprints in the filename. This is useful because the URL changes when the content changes.

That makes immutable caching possible. A browser or CDN can safely cache a hashed asset for a long time because a future build should use a different URL when the asset content changes.

HTML route documents are different. The HTML is the map that points to the current assets. If that map is stale, it can reference yesterday's scripts, old image variants, old font paths, old preload tags, or old service-worker registration behavior.

In that case, the new hashed asset may exist in storage and still not be used. The browser does not guess the new asset URL. It requests the URLs it finds in the page.

This is why a static deployment needs two cache strategies:

  • HTML and service-worker files should be easy to refresh.
  • Hashed assets can usually be cached aggressively.

If those two groups are handled the same way, stale behavior becomes much harder to reason about.

The common failure mode

A quiet stale-HTML failure often looks like this:

  1. The build succeeds.
  2. The pipeline uploads new generated assets.
  3. CloudFront invalidation completes.
  4. A hard refresh still shows old behavior.
  5. The browser network panel shows requests for obsolete asset URLs.

At first glance, that points toward asset caching. But the key question is not only whether the old asset still exists. The key question is why the page is requesting it.

If the page requests /images/generated/blog/example-1024.webp after the build moved to /images/generated/blog/example-a1b2c3d4e5-1024.webp, then something is still serving the old reference. That reference can come from live HTML, a JavaScript bundle, service-worker cache, or embedded route data.

Finding the source of the URL is more useful than refreshing repeatedly.

Inspect live HTML before assuming cache

The fastest way to avoid guessing is to inspect the live HTML response directly.

Do not stop at local build output. Local output proves the build can produce the right files. It does not prove the CDN is serving those files.

Check the deployed route document and ask:

  • Does it contain the expected current asset URLs?
  • Does it contain old framework paths, such as a previous /_next/static/ bundle after a migration?
  • Does it include current footer copy, metadata, or other known markers?
  • Does the visible image URL come from the server-rendered HTML or from client-side hydration?
  • Does the deployed HTML match the file in the build artifact?

Small content markers are useful. A footer tagline, article title, generated asset fingerprint, or script path can quickly reveal whether the origin is serving a new route document or an old one.

If the live HTML is old, invalidating CloudFront may not be enough. Invalidation clears cached edge responses. It does not rewrite stale objects that still live at the origin.

Generated images can hide the problem

Generated images make this class of bug easier to misread. A blog card or article hero may use a generated Open Graph image, a responsive WebP variant, and a manifest that maps source images to optimized output paths.

If a new generated image exists but an old page still references the previous image path, the image pipeline may be blamed incorrectly. The real problem is upstream: the page is still reading an old manifest or an old rendered HTML document.

There is still value in making the image pipeline safer. For example, a build can fail if an expected text layer is missing from generated blog imagery. That catches bad outputs before they deploy.

But a correct generated image does not help if the deployed page never points to it.

Service workers add another place to check

Service workers are useful for offline behavior and controlled caching, but they can complicate stale-deployment debugging. A cache-first service worker can keep serving older static files even after a user refreshes.

When debugging a static site with a service worker, check whether the failing request is coming from:

  • live network HTML,
  • a service-worker cache,
  • a previously cached JavaScript bundle,
  • an old image URL that still exists in object storage.

An incognito window can reduce some browser-cache variables, but it does not prove the origin is current. If incognito still receives old HTML from the CDN, the problem is not the user's local cache.

Service-worker files themselves should usually be deployed with no-cache behavior. If the service worker is cached like a one-year immutable asset, clients may keep old caching rules longer than expected.

S3 deployment filters need careful placement

S3 and CloudFront deployments often split files into groups. A common pattern is:

  • route HTML and sw.js deployed with no-cache headers,
  • hashed assets deployed with long-lived immutable headers.

That split is sound, but the implementation details matter.

In AWS CDK, for example, there is a difference between filtering files while creating a deployment source asset and filtering files during the S3 sync operation. A pattern that looks like an AWS CLI include rule may not behave the same way when placed in asset staging.

If HTML is accidentally excluded from the deployment source, the asset deployment can still succeed while the route documents remain stale. The site may have new images and scripts available in S3, but users still receive old HTML that points somewhere else.

For split deployments, the safer shape is to package the full static build output and apply include/exclude rules at the bucket deployment sync step. That keeps the deployment source complete while still assigning different cache-control headers to different file classes.

Prune stale generated files

Static sites often accumulate old files. Hashed JavaScript, generated images, and previous build artifacts can remain in S3 indefinitely unless the deployment prunes objects that are no longer part of the current build.

Leaving old files around is not always immediately dangerous, but it makes debugging harder. If an obsolete URL still returns 200 OK, a stale page can continue to appear functional. A missing object would have made the stale reference obvious.

Pruning should be configured carefully when a site uses multiple deployments for the same bucket. One deployment should not delete the other deployment's intended files. But once the filters are correct, pruning stale generated assets helps keep the origin honest.

For static sites, a stale object that should no longer exist is useful evidence. It says the deployment process is not fully controlling the bucket contents.

A practical debugging checklist

When a static site keeps showing stale behavior after a successful build, use a sequence like this:

  1. Inspect the live HTML for the affected route.
  2. Compare that HTML with local build output.
  3. Search the live HTML for old framework paths, old asset names, or old content markers.
  4. Verify whether the new hashed assets exist at their expected URLs.
  5. Check whether the old asset URL still exists at the origin.
  6. Review cache-control headers for HTML, service workers, and immutable assets.
  7. Confirm the deployment packages the current build output, not a stale directory.
  8. Confirm include/exclude filters are applied at the right deployment layer.
  9. Prune obsolete generated files when safe.
  10. Re-test in a fresh browser context after the origin is known to be current.

The order matters. Starting with the live HTML keeps the investigation grounded in what the browser is actually receiving.

Why this matters for SaaS teams

For a simple blog, stale HTML might show an old image, old footer, or old script. That is annoying, but usually recoverable.

For SaaS and healthcare-aware products, stale delivery can matter more. A stale page may include outdated notices, old security behavior, stale authentication code, obsolete frontend validation, or a service worker that handles requests with previous assumptions.

Static delivery is still a good option for many public surfaces. It can reduce runtime complexity, improve performance, and simplify hosting. But the deployment model should be treated as infrastructure, not as an afterthought.

Good static deployment is not only about making files available. It is about making sure the right files are available, the old files are removed when needed, and the browser receives the current route document that points to the current asset graph.

Build confidence into the deployment path

The lesson is not to avoid static sites, Vite, Next.js exports, S3, or CloudFront. The lesson is to make the deployment path observable and intentional.

Use no-cache headers for route documents. Cache hashed assets aggressively. Keep service-worker updates fresh. Prune obsolete generated files. Verify live HTML when behavior does not match the build. Treat asset fingerprints as clues, not assumptions.

That approach turns a confusing stale-page problem into a traceable system.

GagliTech helps teams build secure AWS deployment paths, static site delivery, and healthcare SaaS infrastructure that can be understood under pressure. For secure cloud deployment support, contact GagliTech.