Cloudflare Load Balancer is great, but it’s a paid product. If your use case is “I have two replicas behind Cloudflare Tunnel and I just want failover if one goes down”, you can implement a simple edge load balancer using a Cloudflare Worker on the free tier.
The core idea is:
- You expose two different hostnames, one per tunnel (primary and replica).
- You expose a third hostname (the one you share with users) that points to the Worker.
- The Worker performs health checks and proxies each request to the best target.
Architecture (recommended)
You’ll usually want 3 hostnames:
service.yourdomain.com:- Points to the Worker (this is the public URL)
service-primary.yourdomain.com:- Cloudflare Tunnel pointing to your primary server
service-replica.yourdomain.com:- Cloudflare Tunnel pointing to your replica server
This solves the common Cloudflare Tunnel limitation: you can’t bind the same hostname to multiple tunnels, but you can bind different hostnames and put a Worker in front.
How the Worker works
- It keeps an in-memory
serverHealthmap with:healthy: last known health statuslastCheck: timestamp of the last health probe
- On each incoming request:
- It refreshes health status if the cache is older than
HEALTH_CHECK_INTERVAL. - It picks the first healthy server.
- If none are healthy, it falls back to the first server.
- It refreshes health status if the cache is older than
- It adds an
X-Served-Byheader for debugging. - If the proxy
fetchfails, it retries once against the other server.
Important detail: the cache is stored in the Worker isolate memory. That means it’s not a guaranteed global cache (it may reset on cold starts). For simple failover, this is usually fine.
The code
Replace the tunnel URLs below with your own.
Setup (step-by-step)
-
Create two tunnel hostnames
- Primary tunnel hostname:
service-primary.yourdomain.com - Replica tunnel hostname:
service-replica.yourdomain.com
- Primary tunnel hostname:
-
Create a Worker
- Cloudflare Dashboard
- Workers & Pages
- Create Worker
- Paste the code and update the
SERVERSlist
-
Route your public hostname to the Worker
Add a route (or a custom domain) so
service.yourdomain.com/*is handled by this Worker. -
Test which server is serving requests
Look for
X-Served-Byin the response headers. -
Test failover
Stop the primary tunnel temporarily and retry the same request. The header should switch to your replica.
Notes and limitations
- Stateful apps: if your service stores state on disk (sessions, uploads, chat history, etc.), you probably want shared storage or sync between primary and replica.
- Health check endpoint: prefer a lightweight endpoint (or
/) and avoid heavy logic. - WebSockets / long-lived connections: depending on your Cloudflare plan and your app’s behavior, long-lived connections may require extra care.
- Cache scope:
serverHealthis isolate memory, not a global datastore. If you need more reliable shared health state, consider KV, Durable Objects, or external monitoring.
Another way to do it
You could/could use the same Tunnel in two servers/services, so you could have a Tunnel named "Services" and connect it to "Server A" and "Server B". Some people think this solution is better, and in terms of simplicity, it is better. But if you want to separate the Tunnels, Cloudflare Worker Load Balancer is better.
But if you still want a Tunnel in all your servers, here's what I noticed about having 1 Tunnel in all machines: If you use a shared Tunnel in all machines:
Problem: Cloudflare will route jellyfin.domain.com to ANY server (A, B, C or D randomly), but Jellyfin is only on Server A! 💥 Result:
- 75% of the requests will result in error 502 (they fell on the wrong server)
- You lose control of where each service runs
- Total mess
Works but it's not recommended.
In this case, THE CORRECT WAY to use replicas with a single Tunnel would be a Tunnel sharing ONLY when the SAME service is replicated on MULTIPLE machines:
Scenario 1: You want failover from Open WebUI
Works, because Open WebUI is in the two servers.
Scenario 2: Jellyfin is only on 1 server
✅ Correct! Dedicated Tunnel because Jellyfin is only in 1 place.
Summary:
- 1 tunnel per machine: Different services in different machines
- 1 tunnel in multiple machines: Same service replicated (failover)
- 1 tunnel in all machines: Never (unless all services are in all machines)
Remember kindly
Don't forget that you need shared data to be accessible by all servers, otherwise you'll have inconsistencies in the data. And until configurations diverge, or having to configure each one differently.