A client emailed me at 11pm convinced I'd broken their site. "The favicon's still the old one — did the deploy fail? Are we on the wrong server? Did you forget to upload it?" The new favicon was on the server. The CDN cache was cleared. The HTML was correct. The problem? Chrome hadn't re-fetched a favicon in three weeks, and nothing short of a nuclear cache clear was going to make it budge.
I've debugged this exact issue on at least a dozen WordPress sites and custom projects. It's never the favicon file. It's always the browser holding onto the old one like it's a family heirloom. Here's how favicon caching actually works and what I do to fix it when nothing else does.
Images, CSS, JavaScript — browsers follow the usual cache headers for these. Etag, Cache-Control, Expires. Standard HTTP caching. Favicons play by their own rules. According to the HTML spec, browsers can fetch favicons at any time — or not at all. They're not render-blocking, so the browser treats them as optional decoration and caches them aggressively, sometimes ignoring standard cache headers entirely.
Chrome's favicon cache lives in a separate database from the regular HTTP cache. Safari stores them in a system-level icon cache that survives browser restarts. Firefox is relatively well-behaved with Ctrl+Shift+R but then caches them in a favicons.sqlite database alongside history entries.
I tested this across four browsers last month with a freshly uploaded favicon. Chrome took 30 minutes to show the update on a force-refreshed tab. Safari never updated until I cleared the system icon cache. Firefox showed it on the second page load. Edge mirrored Chrome's behavior exactly since they share the same engine.
The most reliable fix I've found doesn't involve clearing any caches. Add a query string version to your favicon link tag:
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=2">
Change ?v=2 to ?v=3 every time you update the favicon. The browser treats it as a completely new URL and fetches it fresh. This works because the URL change bypasses the favicon-specific caching logic — the browser sees a different resource, not an updated version of the same one.
I prefer this over cache-busting with a timestamp (?v=20260620) because it's explicit and intentional. A date-based version tells you when it was changed. An increment tells you how many times it's changed. For a favicon that gets tweaked twice a year, either works.
macOS Safari pins tabs and uses Apple's proprietary mask-icon for that. It's cached in a completely different layer from the standard favicon:
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=2" color="#f59e0b">
I once spent an afternoon wondering why Safari tabs still showed a purple icon after a full rebrand to amber. The mask-icon was unchanged. Safari had dutifully refreshed the regular favicon and completely ignored the pinned tab icon because the mask-icon URL hadn't changed. The fix was the same version-busting query string.
Apple's pinned tab documentation confirms the mask-icon must be an SVG with specific constraints — solid black fill only, no gradients, viewBox="0 0 16 16". If you generate a fresh one with genfavicon.org, it handles these constraints automatically.
If you're on Cloudflare (and most sites are), you have two caches fighting you: the browser favicon cache and Cloudflare's edge cache. I've seen developers clear their browser cache five times while Cloudflare was still serving the old favicon to everyone.
The fix order I use:
/favicon-32x32.png?v=2 to ?v=3 in your HTMLThe incognito test bypasses your local favicon cache entirely. If the new icon shows up there but not in your regular tab, you know the problem is local browser cache and not the server or CDN.
WordPress handles favicons through the Customizer (Appearance → Customize → Site Identity). If you set a favicon there AND add a manual <link rel="icon"> tag in your header.php, WordPress outputs both. Browsers pick whichever one loads last, and it's anyone's guess which one that is.
I've seen WordPress sites with four favicon link tags — one from the Customizer, one from the theme, one from an SEO plugin, and one manually added. Two were pointing to old favicon files. Chrome helpfully picked the oldest one. The fix is to remove all favicon references except one, or use a plugin like RealFaviconGenerator that consolidates everything into a single set.
?v=X query string to every favicon linkDespite everything above, Chrome sometimes holds onto a favicon for up to 24 hours regardless of cache clears, version busts, or incantations. The Chromium favicon component has a dedicated database that's designed to survive almost anything short of a full profile reset.
If you've done the version-bust, purged the CDN, verified it works in incognito, and it's been less than a day — the remaining issue is Chrome's internal favicon DB, and it will update. Just not on your schedule.