Default CDN error pages break trust. A stark white “502 Bad Gateway” or “Error 503: Service Unavailable” tells users your infrastructure failed — and tells attackers exactly what stack you run. Branded, edge-served error pages keep users oriented, preserve SEO equity, and maintain availability even when your origin is completely down.
This guide covers how to design, build, and deploy custom 404, 502, and 503 pages at the CDN edge — with performance budgets, failover logic, and production-ready configuration examples.
Why Error Pages Must Live at the Edge
When you rely on how CDN works in a standard reverse-proxy setup, most error responses still traverse the origin. That works for a 404 where the origin is healthy and simply reports “not found.” It fails catastrophically for 502 (Bad Gateway) and 503 (Service Unavailable), where the origin is unreachable, overloaded, or failing health checks.
Without edge-generated errors, users see one of three bad outcomes:
- Generic CDN error: CloudFront, Fastly, or Cloudflare branding with no navigation, no search, and no reassurance.
- Timeout / blank screen: The CDN waits for origin, then drops the connection after 30s.
- Information leakage: Default pages expose nginx/Apache versions, PoP IDs, and request IDs useful for fingerprinting.
Branded edge error pages solve this by intercepting error status codes at the PoP and returning a fully self-contained response. They load in <100ms, work during regional origin outages, and keep users inside your conversion funnel instead of bouncing back to Google.
For observability, pair custom pages with proper CDN monitoring and observability — log every 5xx interception with PoP, cache status, and origin latency so you can distinguish edge issues from origin saturation.
404 vs 502 vs 503: Different Failures, Different UX
Not all errors deserve the same design. Users interpret them differently, and search engines treat them very differently.
| Metric | Edge Processing | Origin Processing | Improvement |
|---|---|---|---|
| Latency (p50) | 8–15 ms | 120–300 ms | 15–20x faster |
| Cache Hit Ratio | 85–95% | N/A | Reduces origin load |
| TLS Handshake | 1–5 ms (nearby PoP) | 50–150 ms | 10–30x faster |
| Bandwidth Cost | $0.01–0.04/GB | $0.05–0.12/GB | 3–5x cheaper |
| Availability | 99.99% (multi-PoP) | 99.9% (single DC) | 10x fewer outages |
| Error | Meaning at CDN Layer | Cache Strategy | UX Goal |
|---|---|---|---|
| 404 Not Found | Origin returned 404 or edge KV lookup missed. Resource does not exist. | Cache 10–60s at edge; long tail 404s should not hammer origin | Help user find correct content; preserve SEO with soft-404 avoidance |
| 502 Bad Gateway | Origin TCP/TLS failure, invalid response, or timeout. CDN could not get valid HTTP. | Do not cache; serve stale-if-error if available | Reassure, auto-retry, provide status link; hide infra details |
| 503 Service Unavailable | Origin explicitly overloaded, in maintenance, or failing health checks / circuit breaker open. | Do not cache; respect Retry-After header if present | Set expectation for retry, offer alternative actions, show maintenance window |
404: The Navigation Opportunity
A branded 404 should feel like a detour, not a dead end. Include global navigation, a prominent search bar, 3–4 most likely destinations based on URL pattern, and a link to your sitemap. Avoid the temptation to redirect all 404s to the homepage — Google treats that as a soft-404 and you lose crawl budget insight.
Return a true 404 status code. Never return 200 with “Page not found” text. Ensure your CDN is configured to cache the 404 body but keep the status code intact, so CDN performance metrics accurately report error ratios.
502 & 503: The Resilience Pages
These are availability events. The user did nothing wrong. Your tone should be apologetic, concise, and action-oriented: “We’re experiencing high demand. Your request was not processed — please retry in 30 seconds.” Include a Retry-After header (for bots) and a visible countdown or manual retry button (for humans). Link to a status page hosted outside your primary origin — e.g., on a separate domain or CDN-hosted static site.
For 503 during planned maintenance, return 503 with Retry-After: 3600 and a maintenance window. This tells search engines to come back later instead of de-indexing the URL.
Design System for Error Pages
Your error pages are part of your design system, not an afterthought. They should reuse header, footer, and typography — but in a degraded, self-contained form.
Layout & Content Checklist
- Header with logo + primary nav: Inline SVG logo, no external image. Keep 2–3 nav links max.
- Clear status + human message: “404 — Page not found” + one sentence explaining what happened. Avoid jargon like “Bad Gateway.”
- Primary action: 404 → Search + “Go to homepage”; 502/503 → “Retry” button with
window.location.reload()and status page link. - Secondary actions: Contact support, popular categories, or recent articles. Pull these as static links at build time.
- Footer minimal: Copyright + status page URL. No heavy scripts or tracking that could fail.
Performance Budget
An error page is rendered under duress. Users are frustrated and often on mobile. Enforce strict budgets:
- Total transfer < 50KB compressed, < 15KB critical HTML/CSS
- Zero external blocking requests. Inline CSS, inline SVG, system font stack or pre-cached font.
- No JavaScript framework. Vanilla JS only for retry countdown, <1KB.
- Lighthouse performance ≥ 95 even on 3G.
Test with curl -H "Host: example.com" https://cdn-edge/__error/503 and verify content-length and cache-control. Your CDN load balancing algorithms may route 503s to a failover origin — ensure that failover also serves the same branded template to avoid inconsistent UX across PoPs.
Implementing at the CDN Layer
Implementation depends on your provider, but the pattern is identical: intercept status code, serve from edge storage, set correct headers.
1. Cloudflare Workers (Edge Function)
// workers/custom-error.js — serve branded pages from KV export default { async fetch(request, env, ctx) { let response = await fetch(request); if ([404, 502, 503].includes(response.status)) { let key = `error/${response.status}.html`; let branded = await env.ERROR_PAGES.get(key, "arrayBuffer"); if (branded) { return new Response(branded, { status: response.status, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": response.status === 404 ? "public, max-age=30" : "no-store", "Retry-After": response.status === 503 ? "60" : "" } }); } } return response; } }
2. Fastly VCL
# vcl_error — generate synthetic branded response sub vcl_error { if (obj.status == 502 || obj.status == 503) { synthetic {<!doctype html><html>... branded 503 ...</html>}; return (deliver); } } sub vcl_deliver { if (resp.status == 404) { set resp.http.Cache-Control = "public, max-age=30"; } if (resp.status >= 500) { set resp.http.Cache-Control = "no-store, no-cache"; set resp.http.Retry-After = "60"; } }
3. AWS CloudFront Custom Error Responses + S3
Upload 404.html, 502.html, 503.html to an S3 bucket configured as a failover origin. In CloudFront > Error Pages, create custom responses:
# CloudFront custom error — via AWS CLI aws cloudfront update-distribution --id EDFDVBD6EXAMPLE \ --distribution-config '{ "CustomErrorResponses": { "Quantity": 3, "Items": [ {"ErrorCode": 404, "ResponsePagePath": "/errors/404.html", "ResponseCode": "404", "ErrorCachingMinTTL": 30}, {"ErrorCode": 502, "ResponsePagePath": "/errors/502.html", "ResponseCode": "502", "ErrorCachingMinTTL": 5}, {"ErrorCode": 503, "ResponsePagePath": "/errors/503.html", "ResponseCode": "503", "ErrorCachingMinTTL": 5} ] } }'
Set ErrorCachingMinTTL low for 5xx. CloudFront will serve the S3 object directly without hitting the primary origin. For deeper fundamentals, review CDN fundamentals guide and CDN DNS resolution explained to understand how failover origins interact with health checks.
Caching, Headers, and SEO Safeguards
Incorrect headers turn a helpful error page into an SEO disaster.
- Always preserve the original status code. Do not return
200for a missing page. Googlebot needs the 404 to prune the index correctly. - Cache-Control:
404: public, max-age=30, s-maxage=30— short cache prevents origin flood from crawlers.502/503: no-store— ensures recovery is instant when origin returns. - Retry-After: Include on 503/429. Use seconds (
60) or HTTP-date. Bots respect it; users see a countdown. - Content-Type:
text/html; charset=utf-8withX-Content-Type-Options: nosniff. - Surrogate keys: Tag error pages with
surrogate-key: error-pageso you can purge all variants instantly after a deploy.
Enable stale-if-error and stale-while-revalidate at the edge. If origin returns 502 but you have a stale 200 in cache from 2 minutes ago, serving stale content is almost always better than showing an error. Configure this at the CDN, not just in origin headers.
Testing and Operational Checklist
Deploy error pages like any other critical service — with versioning and automated tests.
- Synthetic tests: Curl each error path from multiple PoPs. Assert status code, body contains brand string, and
content-length < 50000. - Chaos test: Block origin security group or stop origin service. Verify 502/503 branded page appears in <200ms with no origin dependency.
- Visual regression: Screenshot 404/502/503 on mobile + desktop; diff against baseline.
- Purge test: Update KV/S3 object, purge surrogate key, verify new version propagates in <5s globally.
- Accessibility: Error pages must meet WCAG AA — focus order, color contrast, and screen-reader announcement of error status.
Store templates in Git, build them with your static site pipeline, and deploy to edge KV/S3 via CI. Version them as error/503.v2.html and atomically switch the Worker/VCL reference to allow instant rollback.
Accept-Language. Pre-build /errors/404.en.html, /errors/404.es.html and select the variant in your Worker/VCL. It adds minimal storage cost and dramatically improves UX for global audiences.
Branded error pages are a small investment with outsized return: lower bounce rates, fewer support tickets, cleaner logs, and better resilience during incidents. Treat them as first-class edge applications — not static afterthoughts — and your CDN will fail gracefully instead of failing visibly.
Frequently Asked Questions
Should CDN error pages be cached at the edge?
404 pages can be cached briefly (e.g., 10–60 seconds) to protect origin, but 502/503 pages should use no-cache or very short TTL (0–5s) with stale-if-error fallback. Always set Cache-Control: no-store for 5xx and use CDN tiered shielding to avoid thundering herd on recovery.
How large should a CDN custom error page be?
Keep it under 50KB total (HTML+CSS+inline SVG) and under 10KB for critical 502/503 failover pages. Avoid external fonts, JS, or images that depend on origin. Inline critical CSS and use system fonts or already-cached Google Fonts with font-display: swap.
Can I show different error pages per domain or path on the same CDN?
Yes. Most CDNs support custom error handling via Edge Workers, VCL, or Rules. You can map host/path to specific branded templates, languages, or retry logic. Use the Host and URL path as keys in your edge function router.
What's the difference between origin-generated vs edge-generated error pages?
Origin-generated pages are returned by your server and proxied by the CDN, preserving dynamic content but failing if origin is down. Edge-generated pages are stored and served directly from CDN edge memory/storage, guaranteeing availability during origin outages for 502/503 scenarios.