I changed my website's code but it's not updating: how to find out why

You saved, built and deployed, and the page still looks the same. Test each layer in order, from your editor to your browser's cache, to find the stale copy, starting with a hard refresh.

A laptop still showing the old version of a page next to a code editor with the change saved, joined by a chain of layers (build, deploy, CDN, service worker, browser cache) where the stale copy could be hiding

You changed the code. You saved it, maybe built and deployed it too. You reload the page and it looks exactly the same.

Either the change never made it out, or it did and something between the server and your screen is still handing you an old copy. There are only a few places that copy can be: your build, your deploy, a CDN, a service worker or your browser’s cache. Guessing takes a long time. Checking them in order is quick.

This guide goes from the cheapest check to the most involved one. Stop at the first one that shows your change.

The short answer

  1. Make the change easy to spot. Put an obvious marker in the page, like a version number or a word in the title.
  2. Hard refresh. ⌘⇧R on a Mac, Ctrl+Shift+R or Ctrl+F5 on Windows and Linux. A phone has no shortcut for this. See how to hard refresh on iPhone, iPad and Android.
  3. Open a private window. It starts with no cache, no cookies and no service worker.
  4. Ask the server directly with curl. If curl shows the old version too, the problem isn’t your browser.
  5. Check what served the page in DevTools’ Network panel: the disk cache, a service worker or a CDN.
  6. Purge the CDN, remove the service worker or fix the build. Which one depends on what steps 3 to 5 told you.
What you see Where the old copy is
A hard refresh or private window shows the change Your browser’s cache
A hard refresh shows it, but the next normal reload is old again A service worker
A private window shows it, a hard refresh doesn’t Something in your session: cookies, an extension or a service worker
curl shows the old version The server or the CDN, not your device
curl with a made-up query string shows the change, plain curl doesn’t The CDN
Nothing shows it, not even the server’s files The build or the deploy

Step 1: Make the change impossible to miss

A lot of “it’s not updating” turns out to be “it updated, I just couldn’t tell.” A one-pixel padding change or a slightly different shade of blue is easy to miss, especially on a phone.

Before you test anything, add something you can’t mistake: change the page’s <title>, add a visible v2 to the footer, or print the build’s commit hash somewhere on the page. Now every check below has a clear yes or no.

While you’re at it, confirm the boring things, because they’re the most common cause:

  • The file is saved, and it’s the file the page actually uses. Search the project for the text you changed. There might be two copies.
  • You’re on the right branch. Run git status and check the change is there.
  • You’re looking at the right URL. localhost, a preview deploy, staging and production are four different sites. Check the address bar before anything else.
  • The dev server is running from this folder. If two are running, the one on the port you opened may belong to another checkout.

Step 2: Hard refresh

A normal reload lets the browser reuse the files it already has, as long as the server’s cache headers say they’re still fresh. A hard refresh tells it to fetch the page and everything on it again.

Browser Shortcut
Chrome, Edge ⌘⇧R, or Ctrl+Shift+R or Ctrl+F5
Firefox ⌘⇧R, or Ctrl+Shift+R or Ctrl+F5
Safari on macOS ⌥⌘R (Reload Page From Origin)

With DevTools open in Chrome or Edge, you can also right-click the reload button and choose Empty Cache and Hard Reload, which clears that site’s cache first.

On a phone there is no hard refresh in any mainstream browser, only workarounds, and most of them cost you something. They’re all covered in how to hard refresh on iPhone, iPad and Android, and hard refresh on desktop vs mobile compares what each browser supports on each platform.

If the hard refresh shows your change, you’re done: it was your browser’s cache. If not, keep going. A hard refresh can’t get past a CDN, and it only skips a service worker for that one load: the next normal reload goes back through it.

Step 3: Open a private window

A private or incognito window starts with an empty cache and no service worker, so it’s a cleaner test than a hard refresh. It also leaves your normal session alone.

  • Shows the change: the old copy is tied to your normal session. Look at what the private window doesn’t have: cookies (a logged-in page can be cached or built differently), extensions, or a service worker (step 5).
  • Doesn’t show the change: your device isn’t the problem. Go to step 4.

In Chrome, all incognito windows share one session, so close any that are already open first. If the page is behind a login, you’ll have to log in again inside the private window.

Step 4: Ask the server directly with curl

Your browser has caches, extensions and service workers in the way. curl doesn’t. It asks the server for the page and prints exactly what comes back.

# Is the change in the HTML the server sends?
curl -s https://example.com/ | grep "v2"

# What do the cache headers say?
curl -sI https://example.com/

If grep finds your marker, the server has the new version, and the old copy is in your browser. Go back to step 3 or 5.

If it doesn’t, the old copy is on the server side. Now find out whether it’s the CDN or the origin, by adding a query string the CDN has never seen:

curl -s "https://example.com/?nocache=$(date +%s)" | grep "v2"

Cloudflare, by default, caches each query string as a separate URL, so a new one makes it fetch a fresh copy from your server.

  • The marker shows up now: the CDN is holding the old copy. Purge it (step 6).
  • Still not there: most likely the origin is serving the old version, because the deploy didn’t go out or went somewhere else (step 7). Check the headers first, though: some CDNs, including CloudFront with its default cache policy, ignore the query string, so this test can’t rule them out.

The headers from curl -sI tell you whether a cache answered:

Header What it means
age: 5321 A cache has held this response for 5,321 seconds
cf-cache-status: HIT Served from Cloudflare’s cache
x-cache: Hit from cloudfront Served from Amazon CloudFront’s cache
x-vercel-cache: HIT Served from Vercel’s cache
cache-control: max-age=86400 Browsers and caches may reuse this for a day without asking
A terminal running curl -sI against a page and showing age, x-cache Hit from cloudfront and cache-control max-age headers, next to a decision: if curl shows the new version the stale copy is in the browser, if curl shows the old version but a made-up query string shows the new one it is the CDN, and if both show the old version it is the deploy
curl skips the browser, so it tells you which side of the network the old copy is on.

Step 5: See what served the page in DevTools

If the server has the new version and your browser still shows the old one, open DevTools (⌥⌘I on a Mac, F12 or Ctrl+Shift+I on Windows and Linux) and go to the Network panel.

  1. Tick Disable cache. It only applies while DevTools is open, which is what you want.
  2. Reload.
  3. Look at the Size or Transferred column for the page and the file you changed.

What it says tells you where the file came from:

  • A byte count: downloaded from the network just now.
  • (memory cache) or (disk cache): reused from the browser’s cache. With Disable cache ticked you shouldn’t see these.
  • (ServiceWorker): served by a service worker, which keeps its own cache. Disable cache doesn’t bypass it, and a hard refresh only skips it for one load.
  • 304: the browser asked, and the server said its copy was still current. If the file really did change, the server’s ETag or Last-Modified didn’t, so look at the deploy.

If a service worker is involved, go to Application → Service workers in Chrome or Edge. Tick Update on reload or Bypass for network while you work, or click Unregister. Application → Storage → Clear site data removes the worker, its caches and the site’s cookies in one go. That logs you out of the site, so only do it once you know it’s needed. In Firefox, about:serviceworkers lists them with an Unregister button.

Service workers are behind a lot of “it updates for me but not for my users.” Some frameworks and PWA plugins add one without it being obvious. If your site has one, make sure new versions activate, instead of waiting for every tab to close.

Step 6: Purge the CDN

If step 4 pointed at the CDN, clear its copy:

  • Cloudflare: Caching → Configuration → Purge Cache, either everything or specific URLs.
  • Amazon CloudFront: create an invalidation for the changed paths, or /*. For example: aws cloudfront create-invalidation --distribution-id <id> --paths "/*".
  • Vercel and Netlify: a new deploy normally replaces the cached pages automatically. If it doesn’t, check you’re on the production URL rather than an older preview deploy.

Invalidations take a moment to spread to every edge location. Run the curl check again after a minute or two.

Step 7: Check the build and the deploy

If the origin itself is serving old files, the change never got there. Check:

  • Did the deploy run, and did it pass? Look at the latest run in your CI or hosting dashboard, and check its commit matches the one with your change. A failed build often leaves the previous version live, with no sign on the site.
  • Did it deploy the right branch? Pushing to dev when the host builds from main is a common way to lose an afternoon.
  • Is the dev server serving a stale build? Hot reload sometimes misses a change, especially to config files, environment variables or new files. Stop the server, delete the framework’s build cache (for example .next for Next.js, or node_modules/.vite for Vite), and start it again.
  • Are environment variables baked in at build time? In most front-end frameworks, changing a public variable in the dashboard does nothing until you build again.
  • Does the domain point where you think? After a move to a new host, run dig +short example.com and check the answer is the new host, not the old one.
Six layers between your code and your screen, each with a check: saved in the editor, checked with git status and a visible marker; build, checked by restarting the dev server and clearing its cache; deploy, checked in the CI run and its commit; CDN, checked with curl and a made-up query string; service worker, checked in DevTools Application; browser cache, checked with a hard refresh or private window
Every layer can hold an old copy, and each has its own check.

When it’s only your phone showing the old version

Phones add two complications. There’s no hard refresh to try first, and the old copy can live somewhere clearing the browser doesn’t reach:

  • A home-screen web app keeps its own storage on iOS, separate from Safari’s. Clearing Safari’s data may not change what the home-screen icon shows.
  • Clearing site data logs you out, which makes testing anything behind a login slow.

A private tab on the phone is still the fastest test. If you need to keep your login, how to hard refresh on iPhone, iPad and Android goes through the least destructive option in every mobile browser.

Stop it happening again

If you run the site, a few settings make this problem mostly go away, for you and for everyone who visits:

  • Serve HTML with Cache-Control: no-cache. The browser still keeps a copy, but asks the server before each use, so a new deploy shows up on the next reload.
  • Give CSS, JavaScript and images content-hashed names, and cache those for a long time. A new build produces new URLs, so nothing old gets reused.
  • Invalidate the CDN as part of every deploy, for the paths that changed.
  • Put the build’s version or commit hash in the page, in a footer or an HTML comment. “Which version am I looking at?” then takes one glance.

On a laptop, most of this checking takes a minute: a shortcut, a private window, one curl. On a phone it’s slower, because the shortcut doesn’t exist and the workarounds log you out. That gap is why we’re building Ion, an iPhone and iPad app for testing your own projects from your phone, with a reload that skips the cache and keeps your login. Ion is in early access; see Ion for developers if you test on your phone a lot.

Get early access.

Invite-only, released in small waves. Free during early access.

Get early access.

Invite-only, released in small waves. Free during early access.