Generating a thumbnail is a one-time cost. Serving it is a continuous cost. For a platform with a billion photos, every profile picture load, every post thumbnail, every image preview is a serve request. Getting the serving architecture right determines whether your image infrastructure is a background cost or a constant fire drill.

URL Structure as Cache Key#

Every image variant needs a stable, unique URL. The URL structure defines the cache key for the CDN.

A common pattern: /images/{photo_id}/{width}x{height}/{format}

/images/a7f3b2/150x150/webp   → 150px thumbnail in WebP
/images/a7f3b2/800x600/jpeg  → medium preview in JPEG
/images/a7f3b2/original/jpeg → full original

The CDN caches each URL indefinitely (or until explicitly invalidated). The first request for a URL hits the origin server, which generates the variant on demand and returns it. The CDN caches the response. All subsequent requests for that URL hit the CDN edge cache.

On-demand generation means you don’t pre-generate every variant for every image. You pre-generate the most common variants (the thumbnail) during the processing pipeline and let the CDN warm the cache for others through organic traffic.

graph TD A[User requests /images/a7f3b2/150x150/webp] --> B[CDN Edge Node] B --> C{Cache hit?} C -->|Yes - most requests| D[Return cached thumbnail: ~10ms] C -->|No - first request| E[Origin Image Server] E --> F[Resize and convert to WebP] F --> G[Cache in CDN: TTL indefinite] G --> D style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Image Immutability#

Once an image is uploaded and processed, its content doesn’t change. Mutable data (user avatar changes) should get a new URL, not change the content at an existing URL. This is critical: CDN caches assume a URL’s content is stable. If you update /images/profile:1001/150x150/webp with new photo content, the CDN still serves the old cached version to most users.

The solution: include a version or hash in the URL. /images/profile:1001/v2/150x150/webp is a new URL with a new cache entry. The old URL naturally expires or is explicitly purged.

Format Negotiation#

WebP is 25-35% smaller than JPEG at the same quality. AVIF is another 30% smaller than WebP. But older browsers don’t support these formats. The serving layer can negotiate: inspect the Accept header, serve WebP to browsers that support it, JPEG to those that don’t.

The URL in this case must encode the format or include a browser-agnostic token, since the CDN needs to cache both the WebP and JPEG versions separately.

At Oracle#

We served network configuration diagrams as generated images. Each diagram was regenerated whenever the underlying config changed. Initially we served them directly from the generation server. When we put a CDN in front, we forgot to implement cache invalidation on config change. Operators were seeing outdated diagrams. A versionless URL was the culprit: the CDN cached /diagram/nssai-1001.png and never saw that the underlying config had changed. We added a ?v={config_version} parameter to invalidate the cache on change. Cache hit rate went from 0% to 92% while ensuring freshness.

What I’m Learning#

Image serving is a solved problem if you design the URL structure correctly from the start. Immutable content at stable URLs makes CDN caching trivially correct. Mutable content requires version-encoded URLs. The origin server’s job is to handle the cold cache miss efficiently; everything after that is the CDN’s job.

Have you had cache invalidation bugs where users saw stale content because the URL structure didn’t encode content version?