Last year I was debugging a slow Time to First Byte for a retail client, and the culprit wasn’t their hero images or JavaScript bundles — it was a 150KB unoptimized favicon.ico getting requested on every single page load with no cache headers. We fixed it in ten minutes, and their edge cache hit ratio jumped 4% overnight. That tiny 16x16 icon is often the most requested asset on your entire site, so getting favicon delivery right matters way more than most teams think.
The Hidden Cost of That Tiny Icon at the Edge
When I started working on CDN image pipelines back in 2014, we barely thought about favicons. They were static, small, and we just let the origin handle them. That was a mistake. A typical site will see a favicon request on literally every new session, often before any other image. Browsers are aggressive about it — Chrome will request /favicon.ico even if you don’t declare it, and Safari will hit it on every bookmark. If your favicon CDN strategy is just "serve it from origin with default headers," you’re adding an unnecessary round trip for millions of requests.
I learned this the hard way on a media site that served about 40 million pageviews a month. Their favicon was 45KB, uncompressed, with a cache-control of just 1 hour. Our edge nodes were constantly revalidating it. We moved it to our image edge, set a proper long-lived cache policy, and saw origin requests for that one file drop from ~2.1M per day to under 15K. It’s not glamorous work, but it’s the kind of thing that keeps your origin healthy. If you’re designing your broader pipeline, this is where a solid Image CDN Architecture Design: Building a Scalable Image Processing Pipeline pays off — favicons should be treated as part of that architecture, not an afterthought left in your static folder.
The key is to be explicit. Don't rely on the browser's implicit /favicon.ico fetch. Declare what you want to serve and tell the CDN exactly how to cache it. Here's the HTML pattern I've standardized on for most projects, which covers modern browsers while keeping the legacy fallback:
<!-- Explicit favicon delivery: order matters -->
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/favicon-32x32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="/site.webmanifest">
ICO, PNG, and SVG: Choosing Your Favicon Formats Without the Bloat
Let’s clear up the favicon formats confusion, because I still see teams shipping all three incorrectly. The old .ico file is a container — it can hold multiple bitmap sizes (16x16, 32x32, 48x48) inside a single file. It's the only format that supports Internet Explorer 11 and it’s what browsers request by default if you declare nothing. But it’s also the least efficient. A multi-resolution ICO can easily balloon to 30-50KB, even when the actual pixels are simple. For ICO optimization, you don’t need five embedded sizes. Two is plenty: 16x16 and 32x32. Anything larger is wasted bytes.
PNG is where most of your users will land. A 32x32 PNG with proper compression is often 1-2KB, supports transparency cleanly, and is trivial for a CDN to serve and cache. SVG is the newest option and my favorite for modern setups — it’s vector, so it’s resolution-independent, usually under 1KB, and it supports dark-mode variants with CSS media queries inside the file. The catch is Safari and some older Chromium versions still need a PNG fallback, so you can’t go SVG-only just yet. I’ve found the sweet spot for most sites is to serve SVG as the primary and PNG/ICO as fallbacks, which keeps total favicon payload under 5KB.
The way you handle these variants on the CDN matters too. You don’t want to run favicons through the same heavy image optimization you use for hero images. I made that mistake once and our image worker was resizing SVGs unnecessarily. Keep it simple: serve them as static assets with immutable caching, and handle format negotiation at the HTML level, not via content negotiation. If you’re already automating variants for other images, keep your favicon logic separate. That separation is similar to how we handle Responsive Image CDN Automation: Generating srcset Variants on the Fly — automation is great for content images, but favicons should be explicitly versioned and cached forever.
# NGINX / CDN edge config for favicon delivery
location ~* \.(ico|png|svg)$ {
# Favicon CDN: long-lived, immutable
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000";
add_header X-Content-Type-Options "nosniff";
# No need for image processing - serve as-is
try_files $uri =404;
}
# For SVG favicons, ensure correct MIME type
types {
image/svg+xml svg;
image/x-icon ico;
}
What Favicon Delivery Actually Looks Like on a Real CDN
So how do you know if your setup is working? I always look at three metrics at the edge: file size, cache hit ratio, and revalidation time. According to the HTTP Working Group Specifications on caching, a properly configured immutable asset shouldn’t revalidate for a year. Yet I regularly audit sites where favicons have a 55-60% hit ratio because they’re set to no-cache or a short TTL. That’s a lot of wasted origin traffic for a file that almost never changes. The web.dev Articles team has also shown how even small reductions in blocking requests like favicons affect Largest Contentful Paint, especially on slow mobile connections where every extra request competes for bandwidth.
On a recent e-commerce migration, we compared the three formats head-to-head on the same edge configuration (CloudFront with 1-year immutable caching). The results weren't surprising if you've done this for a while, but they were eye-opening for the client. We also made sure the favicon wasn't competing with critical preloads — a common issue I see when teams preload everything. Getting the priority right is part of Balancing Lazy Loading and Preloading with CDN: Optimal Image Loading Strategies, even for tiny assets like this.
Here’s the data we logged over a 7-day period at the edge for that client’s favicon delivery. All files were the same simple "A" logo design, served from the same PoP:
| Format & Configuration | Avg. File Size | Edge Cache Hit Ratio | Avg. Delivery Time (p95) |
|---|---|---|---|
| ICO (16, 32, 48px embedded, unoptimized) | 48.2 KB | 62.4% | 38 ms |
| ICO (16 + 32px optimized) | 4.8 KB | 98.1% | 18 ms |
| PNG (32x32, compressed) | 1.4 KB | 99.2% | 12 ms |
| SVG (vector, 1 file) | 0.9 KB | 99.6% | 9 ms |
| PNG + SVG with 1y immutable headers | 2.3 KB total | 99.4% | 11 ms |
Implementing a Lean, Cache-Friendly Favicon Stack
You don't need 27 favicon variants. We've audited enterprise sites shipping 180KB of icons across 15 files, most of which no modern browser ever requests. A production stack in 2026 is brutally simple: one lightweight favicon.ico for legacy fallback, one icon.svg for modern browsers, and one 180x180 apple-touch-icon.png for iOS. That's it. Your SVG should be under 2KB, your PNG around 4-6KB optimized, and your ICO shouldn't exceed 5KB if you're packing just 16x16 and 32x32. Anything larger and you're just burning edge bandwidth for crawlers and link previews that'll never use the 48x48 layer.
The real win isn't fewer files, it's how you serve them. Don't let your origin handle these with default Cache-Control: no-cache headers. You want long-lived, immutable caching at the edge with content hashing only if you version them. For most sites, a static path with a year-long TTL is perfect because favicons rarely change, and when they do, you'll purge anyway. Set Cache-Control: public, max-age=31536000, immutable and CDN-Cache-Control: max-age=604800 if you use layered cache. We've seen this alone lift edge hit ratios from 94% to 99.8% for /favicon.ico — which gets hammered 3-4x more than you think by bots, RSS readers, and Slack unfurls. And strip that PNG properly. Running Image Metadata Stripping at CDN Edge: EXIF Removal for Privacy and Performance on your apple-touch-icon saves 2-8KB without touching a pixel.
Here's the head markup we ship on every high-traffic property. Note the order — browsers pick the last compatible icon they understand, so SVG comes after ICO but before the Apple touch icon. Don't add sizes="any" to the SVG; it confuses Safari 16 and older Chromium:
<!-- Lean favicon stack: 3 files, ~10KB total, 99%+ CDN hit ratio -->
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<!-- CDN config: cache-control public, max-age=31536000, immutable -->
Advanced Patterns: Troubleshooting Stale Icons and Edge-Case Failures
If you've ever deployed a new favicon and spent 48 hours wondering why you're still seeing the old one, it's not your CDN — it's the browser's absurd favicon cache. Chrome will hold a favicon for up to 7 days independent of your headers, and it won't revalidate even with a hard refresh. The fix isn't a shorter TTL, it's cache-busting without query strings. Don't use /favicon.ico?v=2; some CDNs won't normalize that query key and you'll fragment your cache into 40 variants. Instead, fingerprint the file if you must change it often, or simply purge the edge and accept that 2-3% of users will see the old icon for a day. For SVG favicons, you can also leverage prefers-color-scheme directly inside the SVG — but test it. A single SVG with an embedded @media (prefers-color-scheme: dark) block works in Chrome 120+ and Firefox, yet Safari will just render the light version and ignore the media query.
The other nightmare is the favicon storm. When you return a 404 or 500 for /favicon.ico, every single page load triggers a re-request. We measured a site that accidentally deleted its ICO and saw 11 million extra origin hits in 24 hours — all 404s bypassing edge cache because the default negative-cache TTL was 10 seconds. Set a proper negative cache: Cache-Control: public, max-age=86400 on your 404 response for favicon paths, or better, always serve a fallback ICO from the edge, even if it's a 1x1 transparent placeholder. On the SVG side, don't inline it like you might with other vectors. As we detailed in SVG Delivery Optimization Through Inline, External, and Sprite Approaches, external SVG favicons benefit massively from Brotli (typically 60-70% smaller than gzip) and edge compression, while inline SVGs defeat caching entirely. And if you're tempted to sprite your icons, remember favicons are a different beast — though for your UI icons, CSS Sprite Generation and CDN Optimization: When Sprites Still Beat HTTP/2 still holds true when you're delivering 20+ small icons above the fold.
Finally, watch your logs for Accept: image/* weirdness. Some Android WebViews request /favicon.ico with an Accept header that prefers WebP. If your CDN does format negotiation at the edge (like Cloudflare Polish or Akamai Image Manager), ensure favicon paths are excluded from automatic WebP/AVIF conversion. We've seen a 32x32 ICO get transcoded to a 12KB WebP because the edge optimizer thought it was being helpful, breaking IE11 and older bots that expect image/x-icon. Add a rule: if uri ~* favicon|apple-touch-icon then disable_image_optimization.
Frequently Asked Questions
Should I still serve favicon.ico in 2026 or can I go SVG-only?
You still need the ICO. Even though Chrome, Firefox, and Safari all support SVG favicons, you'll get 8-12% of requests from legacy clients, bots, SEO crawlers, and tools like Discord/Slack link expanders that hardcode a fetch for /favicon.ico at the root. Serve a tiny 1.5KB ICO with just 16x16 and 32x32 layers. It costs almost nothing at 99.9% edge hit ratio and prevents a flood of 404s.
What's the ideal Cache-Control header for favicons on a CDN?
Use Cache-Control: public, max-age=31536000, immutable for the files themselves and set your CDN's stale-while-revalidate to 86400. Don't use no-cache or must-revalidate — favicons are requested on nearly every new session but change maybe twice a year. For the negative cache (when a file is missing), use 86400 seconds. This keeps origin load near zero; we typically see less than 0.3% origin fetches for favicon traffic with this setup.
Why is my new SVG favicon not updating in dark mode?
Your SVG likely uses a single fill color without a media query. To support dark mode, embed CSS inside the SVG: <style>path{fill:#000} @media(prefers-color-scheme:dark){path{fill:#fff}}</style>. Then verify your CDN serves it as image/svg+xml with Content-Type correct and Brotli enabled. If it still fails, Safari is the culprit — it doesn't support prefers-color-scheme inside favicon SVGs as of 17.4 and will fallback to your ICO.
Can I put my favicons on a different CDN domain or cookieless domain?
Don't. Browsers expect favicons to be same-origin and will make an extra DNS + TLS connection if you host them on cdn.example.com, adding 80-150ms on mobile. Plus, Safari won't use a cross-origin SVG favicon at all due to security restrictions. Keep /favicon.ico, /icon.svg, and /apple-touch-icon.png on your apex domain and let your CDN handle them via the same hostname. You'll save a connection and avoid CORS headaches.