Intro
For a while this site lived on two very different hosts: a static copy on GitHub Pages and a noisy mini-PC humming away in my dorm. The mini-PC pushes out dynamic pages and stats, while GitHub Pages serves a static snapshot. Cloudflare Tunnel glued them together, but any time the dorm box went dark visitors were greeted by the dreaded Error 1033: Origin DNS error. It was the opposite of graceful degradation.
1033 is Cloudflare's way of saying, "I can't reach your origin." When you're paying nothing for compute and you're using a makeshift tunnel over campus Wi-Fi, that happens more often than I'd like. I didn't want friends or recruiters hitting an error page just because I rebooted the box or the tunnel flaked out overnight.
I wanted a free, automatic failover. If the tunnel is healthy, send users to the mini-PC. If not, keep them on GitHub Pages with zero error pages or manual fiddling. No load balancer bill, no servers idling. Just smart routing.
This mindset is consistent with the rest of the site. I'm a fan of scrappy infrastructure; my GitHub-powered SQL backup system and the Cloudinary globe experiment were built the same way - lean, automated, and designed to fail gracefully. Those projects nudged me toward building things that watch themselves and fall back elegantly.
Hybrid hosting also scratches a creative itch. I love that a static site on GitHub can spring to life when the dorm server is awake. When I'm traveling or testing something risky, I can yank the power cord and the site calmly keeps serving the static build. When I plug it back in, users start hitting fresh endpoints without needing to refresh.
Frontend Health-Ping (Go Live When Up)
The first piece lives on GitHub Pages. Every page includes a tiny script that pings health.png on the live server. If the image loads within 1.5 seconds, the script swaps the user over to the tunnelled site. If it hangs or errors, the page simply stays put on GitHub.
I went with an image instead of an AJAX request because it dodges CORS headaches and works even if JavaScript is heavily sandboxed. The timeout is short on purpose-long enough to survive dorm Wi-Fi but short enough that users don't watch a blank tab spinner. After a few evenings of testing, 1500 ms was the sweet spot.
The health file itself is aggressively cached. It's a 1-byte PNG that Cloudflare caches for 30 seconds, so the script only hits the origin when absolutely necessary. If the cache returns fast, the redirect happens almost instantly; if the cache misses or the origin is offline, the timer expires and users remain on GitHub.
<script>
(function () {
var LIVE = 'https://live.aboutsharma.com/';
var giveUp = setTimeout(function () { /* stay on GitHub */ }, 1500);
var img = new Image();
img.onload = function () { clearTimeout(giveUp); location.replace(LIVE); };
img.onerror = function () { /* stay on GitHub */ };
img.src = LIVE + 'health.png?ts=' + Date.now();
})();
</script>
<noscript>Live server: <a href="https://live.aboutsharma.com/">Open</a></noscript>
Nothing fancy: GitHub pages hosts the script, the dorm server exposes a single health.png, and the browser does the rest. Fast response? We move to the low-latency tunnel. Slow or missing? We silently stick with GitHub.
Failover with Cloudflare Worker
Redirecting at the edge keeps the tunnel honest. A Cloudflare Worker sits in front of live.aboutsharma.com and tries the dorm server first. If the fetch succeeds, the response streams back. If it throws or comes back non-200, the Worker bounces the request to GitHub Pages, preserving the original path and query string.
Setting it up took three short steps:
- Create a Worker under Workers & Pages.
- Bind it to the route
live.aboutsharma.com/*. - Flip the failure mode to fail closed so any fetch hiccup triggers the redirect.
export default {
async fetch(request) {
const url = new URL(request.url);
try {
const res = await fetch("https://live.aboutsharma.com" + url.pathname + url.search, request);
if (res.ok) return res;
return Response.redirect("https://www.aboutsharma.com" + url.pathname + url.search, 302);
} catch {
return Response.redirect("https://www.aboutsharma.com" + url.pathname + url.search, 302);
}
}
};
The Worker is deployed on Cloudflare's free tier and bound to live.aboutsharma.com/*. Because it "fails closed," any tunnel outage immediately falls back to GitHub. No more 1033 splash screens.
Fail closed is crucial. If the Worker were set to fail open, a network hiccup would surface a Cloudflare-branded error page instead of my GitHub fallback. In a chain that already includes campus Wi-Fi, a tunnel, and an aging mini-PC, I want the edge to be decisive.
Why This Method
Cloudflare has a paid load balancer and fancy health checks, but this setup costs nothing and reacts instantly. DNS failover can take minutes to propagate; a Worker runs at the edge and makes the decision per-request. Custom error pages are also paywalled-handing control to a Worker sidesteps that cost.
- Custom Error Pages require a paid plan, the Worker doesn't.
- DNS Failover is slow; Workers check health on every request.
- Cloudflare Load Balancer is great, but also billed. A Worker does 90% of what I need for free.
This approach also keeps URLs stable. Users don't end up dumped on the homepage when something breaks; the path and query survive the redirect. It's a tiny detail that saves debugging time when someone bookmarks a deep link.
Because the Worker issues a 302, browser caches don't cling to the redirect. Once the dorm box returns, the next request hops back automatically. It feels almost like real load balancing, minus the monthly invoice.
Technical Deep Dive
The free Worker tier allows 100,000 requests per day, which is plenty for my traffic. Cold starts are a non-issue: my measurements average under 20 ms before the fetch fires. To test failover I simply killed the tunnel service and watched requests hop back to GitHub in real time.
Cloudflare's network is fast enough that the extra hop through the Worker is invisible. My heaviest page - the globe demo from the Footprints write-up - still loads in under a second when routed through the Worker.
Cold start behavior barely registers. Workers spin up in new POPs occasionally, but logs show the first request taking maybe 30 ms longer. After that it's all warm starts. For a setup running on a few hundred hits per day, that's effectively zero penalty.
I also simulated slow health checks by delaying the health.png response. The script's 1.5 second timeout felt right-quick enough to avoid annoying users but long enough to tolerate dorm Wi-Fi jitters.
Testing failover meant hard-killing the tunnel process, unplugging Ethernet, and even blocking the port with iptables. In each case the Worker handed requests off to GitHub without exposing a stack trace or a Cloudflare code. The logs make it obvious when the dorm server is sick.
If I were running something more serious, I'd reach for HAProxy or Nginx with active-passive nodes. But for a student budget, Cloudflare gives me global edge logic without babysitting a VPS. It's the same scrappy spirit behind that SQL backup job, just stretched across the network layer.
There's also a nice symmetry here. The mini-PC isn't powerful enough for big frameworks, but it runs Go binaries and small Node scripts just fine. GitHub Pages handles everything else. Cloudflare sits in the middle and lets me glue free tiers together like LEGO.
Wrap-up
The 1033 errors are gone. When the mini-PC is awake, users land on a fast, dynamic site. When it's asleep or the tunnel glitches, they glide over to GitHub Pages automatically.
Next on my list: self-healing tunnels, status banners that reflect which host you're on, and maybe syncing a tiny SQLite database between both sides. Until then, the whole setup runs free on a student's budget-and quietly keeps serving pages while I sleep.
If you end up trying something similar, I'd love to hear how you stress-test it. Drop me a note or open an issue on the repo; like most of my projects, this one is a work in progress.