This is a condensed cross-post of the original KeyboardTester.click case study: Cloudflare Workers Failover for Shared Hosting: Our Live Setup.
A shared-hosting provider gave us a six-hour maintenance window but could not predict the actual interruption. A timed DNS switch to one maintenance page would have been simple, yet it would also have replaced more than 1,500 useful pages and browser tools with the same message.
We chose a narrower continuity layer: an always-on Cloudflare Worker that tries the real origin first and uses a prebuilt static snapshot only when the origin is unavailable.
This is not a second writable application stack, and it is not universal zero downtime. Forms, email, server-side AI, database writes, sessions, and saved actions still need the hosting server. The goal is to keep safe read-only journeys useful and make recovery automatic.
The request flow
For each request, the Worker follows this contract:
- Request the shared-hosting origin with a bounded timeout.
- Pass healthy responses, redirects, and legitimate 4xx responses through unchanged.
- On a network error, timeout, selected 5xx response, or Cloudflare 520–527 status, look for the same path in the static snapshot.
- Serve the matching snapshot only for safe
GETorHEADrequests. - Return an honest
503 Service UnavailablewithRetry-AfterandCache-Control: no-storewhen a write cannot reach the origin. - Try the origin again on later requests. The next healthy origin response passes through immediately.
The last point removes a fragile event-day task. No one has to notice a recovery email and manually disable a maintenance page.
Build the snapshot from rendered production
Our local PHP tree contained unpublished work, so copying it would not have represented the live site. The builder crawled rendered canonical production instead.
The tested package captured:
- 1,514 rendered pages;
- 1,234 referenced first-party assets;
- 2,749 total files after manifests and support files;
- about 547.6 MB in total;
- no individual file above Cloudflare's asset limit.
The crawler preserved canonical links, hreflang, structured data, internal links, scripts, styles, fonts, and client-side tool assets. It removed service-worker registration and production tracking from the stored copy. A restrained continuity notice was added so visitors could tell that dynamic functions might be unavailable.
Production analytics and advertising can be injected by the Worker only when fallback HTML is actually served. Private staging should remain authenticated, noindex, tracker-free, and ad-free.
Do not treat every non-200 response as an outage
A redirect, a 404, or an authorization response can be correct application behavior. Replacing all of them with the snapshot can create false pages and hide real errors.
Our fallback set was deliberately limited to:
500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527
A sanitized version of the origin decision looks like this:
const FALLBACK_STATUSES = new Set([
500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527
]);
try {
const origin = await fetch(originRequest, {
signal: AbortSignal.timeout(4500),
redirect: "manual",
cf: { resolveOverride: "origin-bypass.example.com" }
});
if (!FALLBACK_STATUSES.has(origin.status)) return origin;
return serveSnapshot(request, origin.status);
} catch {
return serveSnapshot(request);
}
The hostname is only a placeholder. Do not publish an origin IP, account identifier, route identifier, access token, or real bypass hostname.
The same-zone recursion trap
Once a Worker route covers example.com/*, a naive fetch to https://example.com/... can run through the same Worker route again.
Cloudflare's resolveOverride can direct the origin lookup to a controlled DNS-only hostname in the same zone while preserving the public request URL and canonical Host header. That avoids recursion without changing the URLs users and crawlers see.
Treat the bypass hostname like infrastructure. Verify its TLS and virtual-host behavior, keep it out of public examples, and do not create one casually.
Unknown pages and write actions should fail honestly
Returning the homepage with status 200 for every missing snapshot path would fabricate content and confuse both users and search engines.
When the origin is unavailable:
- a known safe path can return its stored page or asset;
- an unknown safe path returns a controlled 503;
- a state-changing request returns 503 with
Retry-Afterandno-store; - no write is queued or reported as successful unless a real durable design supports that promise.
if (!new Set(["GET", "HEAD"]).has(request.method)) {
return new Response("Temporarily unavailable", {
status: 503,
headers: {
"Retry-After": "300",
"Cache-Control": "no-store",
"Content-Type": "text/plain; charset=utf-8"
}
});
}
Private staging is a release gate
Before attaching public routes, we tested three different states:
1. Forced snapshot mode
We checked the fallback marker, canonical URL, one H1, valid JSON-LD, first-party assets, maintenance notice, missing paths, HEAD, write requests, localized pages, Arabic RTL, and mobile layout.
2. Origin preview
We confirmed that the alternate lookup reached the real hosting origin without Worker recursion.
3. Healthy production pass-through
We confirmed the fallback marker was absent and that normal pages, redirects, headers, ads, and analytics remained origin-generated.
The remote snapshot audit covered 2,748 page and asset paths without failures. We did not deliberately break the public origin to test the design.
Rollback should be independent of deployment
The fastest rollback is removing only the routes owned by the continuity Worker. It should not require deleting the Worker, changing DNS, rebuilding the site, or touching unrelated routes.
Route tooling should:
- list current routes before any change;
- refuse collisions;
- attach apex and
wwwone at a time; - verify each hostname immediately;
- delete only the exact owned routes during rollback.
For our availability use case, the public routes were also configured to fail open at the Worker request limit. If the free daily allowance is exhausted, Cloudflare bypasses the Worker and sends requests to the hosting origin instead of letting a quota error create a new outage. That trade-off is appropriate for continuity logic, but it would be wrong for a Worker enforcing authentication or another security boundary.
SEO, analytics, and ads during fallback
The snapshot stays on the original domain and preserves the existing canonical tags, structured data, hreflang, and internal links. A page that really exists in the snapshot returns 200; an unavailable action or unknown page returns 503.
There is no public backup domain to index and no duplicate URL set to submit. Private staging remains blocked from crawling.
For production fallback HTML, analytics and ads can load with the same delayed posture as the main site. In our implementation, a streaming HTMLRewriter adds them only to fallback GET HTML. It does not modify HEAD, CSS, JavaScript, images, or healthy origin responses.
Ad delivery and revenue are never guaranteed, especially during an outage. Keeping the code available is not the same as guaranteeing fill.
When this pattern fits
This approach is useful when most value is in public content and client-side tools, the canonical site already uses Cloudflare, and a stale-but-recent read-only copy is better than a blanket maintenance page.
It is a poor substitute for real application replication when the product is write-heavy, authenticated, transactional, or dependent on a frequently changing database.
The broader lesson is simple: choose the smallest continuity layer that matches the failure mode, test it privately, preserve honest HTTP semantics, and make both recovery and rollback boring.
The full case study includes the provider decision comparison, implementation checklist, status matrix, visual request flow, evidence timeline, and source links: read the original Cloudflare Workers failover guide.
Top comments (0)