Cloudflare CDN Caching for URL-Generated Placeholder Images
Configure Cloudflare cache rules to serve generated placeholder images from edge locations, cutting origin latency to near zero for global users.
Cloudflare's CDN has over 300 edge locations globally. When a generated placeholder image is cached at the edge closest to the user, the response latency drops from 200–400ms (origin round-trip) to 5–20ms (edge cache hit). For a placeholder service where speed matters as much as content, this is a meaningful difference.
Getting Cloudflare to cache generated images correctly requires understanding the Cache-Control header hierarchy, Cloudflare's default behaviors, and how Workers fit into the cache lifecycle. A Cloudflare Worker generating placeholder images needs to emit the right response headers to be cached automatically and for the right duration.
Cloudflare caching model
How Cloudflare decides what to cache and for how long
Cloudflare evaluates several signals to decide if a response should be cached at the edge. The response must come from an origin (or Worker) that is proxied through Cloudflare. The status code must be cacheable (200, 301, 302, 404 with proper headers, and others). The response must not set cookies, and the Cache-Control header must allow public caching.
By default, Cloudflare caches based on the file extension in the URL. SVG, PNG, JPEG, WebP, and AVIF are cached by default based on their extension. URLs without a file extension — like /api/v1/400x300 — may not be cached unless you configure a Cache Rule to cache them explicitly.
Cloudflare's Edge Cache TTL setting overrides the browser's max-age for how long the CDN edge retains the cached asset. Setting Edge Cache TTL to one year for placeholder image paths means Cloudflare serves the image from cache for up to a year after the first fetch, regardless of what the origin returns next time.
Cache Rules
Configuring Cloudflare Cache Rules for placeholder image paths
Cloudflare Cache Rules (available on all plans) let you define URL patterns and override cache behavior for matching requests. For a placeholder API serving images at /api/v1/*, create a Cache Rule matching that path prefix and set Cache Status to Cache Everything, with a long Edge Cache TTL.
Cache Rules are evaluated in order. Put the most specific rules first. A rule matching /api/v1/* should appear before any catch-all rules. If you have a rule bypassing cache for /api/* for other API routes, the placeholder-specific rule needs to be higher in the list.
# Cloudflare Cache Rule (configured in Dashboard or via API)
# Match: URI Path starts with /api/v1/
# Setting: Cache Everything
# Edge Cache TTL: 1 year (31536000 seconds)
# Browser Cache TTL: Respect Existing Headers
# Resulting headers for a cached placeholder:
Cache-Control: public, max-age=31536000, immutable
CF-Cache-Status: HIT
Age: 86400 # seconds since edge cached this response Workers caching
Emitting correct headers from a Cloudflare Worker
When the placeholder image is generated by a Cloudflare Worker, the Worker controls the response headers. Set Cache-Control: public, max-age=31536000, immutable in the Worker response. Cloudflare reads this header and stores the response in its edge cache for up to a year.
Workers also have access to the Cache API, which lets you programmatically store and retrieve responses from the Cloudflare cache. Using cache.put() inside a Worker lets you cache responses for paths that don't match the default extension-based caching rules.
The key difference between relying on Cache-Control headers versus the Cache API is control. Cache-Control passively instructs Cloudflare; the Cache API actively stores the response in the first request and retrieves it on subsequent ones. Use the Cache API when you need predictable caching behavior regardless of path structure.
// Cloudflare Worker: cache generated placeholder via Cache API
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const cache = caches.default;
const cached = await cache.match(request);
if (cached) return cached;
const svg = generatePlaceholderSVG(request.url);
const response = new Response(svg, {
headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
'CDN-Cache-Control': 'max-age=31536000',
Vary: 'Accept',
},
});
// Store in Cloudflare edge cache
event.waitUntil(cache.put(request, response.clone()));
return response;
},
}; Cache headers
CDN-Cache-Control vs Cache-Control for edge vs browser TTL
Cloudflare reads both Cache-Control and CDN-Cache-Control. CDN-Cache-Control is a Cloudflare-specific header that sets edge cache TTL independently of browser TTL. If you want the CDN edge to cache for a year but only the browser to cache for a day, set Cache-Control: public, max-age=86400 and CDN-Cache-Control: max-age=31536000.
For placeholder images, you typically want both the browser and CDN to cache aggressively. Set Cache-Control: public, max-age=31536000, immutable and omit CDN-Cache-Control unless you need different TTLs. The immutable directive applies only to browser caches; Cloudflare ignores it for its own TTL calculations.
Cache validation
Verifying cache hits with CF-Cache-Status
Every response through Cloudflare's proxy includes a CF-Cache-Status response header. Possible values include HIT (served from edge cache), MISS (fetched from origin), EXPIRED (stale, refetched), BYPASS (cache bypassed), and REVALIDATED.
After deploying your cache configuration, fetch a placeholder URL twice. The first request should return CF-Cache-Status: MISS. The second should return HIT. If you see BYPASS repeatedly, check that the path matches your Cache Rule and that the response does not include Set-Cookie headers, which disable caching.
Use curl to inspect headers without browser interference: curl -I 'https://fallback.pics/api/v1/400x300/7C3AED/FFFFFF?text=Test'. The CF-Cache-Status and Age headers tell you exactly what Cloudflare did with the request.
# Verify cache status with curl
curl -I 'https://fallback.pics/api/v1/400x300/7C3AED/FFFFFF?text=Test'
# Expected headers for a cached response:
# CF-Cache-Status: HIT
# Age: 3600
# Cache-Control: public, max-age=31536000, immutable
# Content-Type: image/svg+xml
# API and docs
https://fallback.pics/docs/
https://fallback.pics/placeholder-image-api/
# Related posts
https://fallback.pics/blog/immutable-urls-cdn-placeholder-caching/
https://fallback.pics/blog/self-hosted-placeholder-image-api-cloudflare-workers/ Global latency
Measuring edge cache latency improvement across regions
Run a latency test against a placeholder URL from multiple geographic locations using a tool like Pingdom or WebPageTest multi-location. On the first request (MISS), latency reflects the round-trip to the origin — typically 50–400ms depending on origin location and tester region.
After the CDN warms up (MISS on first request, HIT on subsequent requests), latency drops to 5–30ms from any tested region. Cloudflare's nearest edge is rarely more than 20ms from any location with decent internet connectivity.
Key takeaways
What to standardize before shipping
- Cloudflare does not cache extensionless API paths by default — create a Cache Rule for /api/v1/* matching Cache Everything.
- Emit Cache-Control: public, max-age=31536000, immutable from your Worker or origin to instruct edge caching.
- Use the Cache API in Cloudflare Workers for deterministic, programmatic cache control independent of path patterns.
- CDN-Cache-Control sets the Cloudflare edge TTL independently from the browser max-age.
- Verify caching is working with CF-Cache-Status: HIT and Age headers using curl before going to production.
Production fallback layer
Use fallback.pics anywhere an image URL is accepted.
Start with one deterministic URL and standardize fallback behavior across your design system.