Load balancer com Cloudflare Worker + health check (de graça)
Um load balancer gratuito usando Cloudflare Workers + Cloudflare Tunnel, com health check em intervalo e failover automático.
O Cloudflare Load Balancer é muito bom, mas é um produto pago. Se o seu caso é “tenho duas réplicas atrás do Cloudflare Tunnel e quero failover automático se uma cair”, dá pra resolver com um Worker (free tier) fazendo roteamento e health check.
A ideia é simples:
- Você cria dois hostnames, um por túnel (primary e replica).
- Você cria um terceiro hostname (o que você divulga) apontando pro Worker.
- O Worker verifica se os backends estão saudáveis e faz proxy para o melhor destino.
Arquitetura (recomendada)
O padrão mais prático é usar 3 hostnames:
service.seudominio.com:- Aponta pro Worker (URL pública)
service-primary.seudominio.com:- Cloudflare Tunnel apontando pro servidor principal
service-replica.seudominio.com:- Cloudflare Tunnel apontando pro servidor réplica
Isso resolve aquele “problema” comum do Tunnel: você não consegue ter o mesmo hostname em múltiplos túneis, mas consegue ter hostnames diferentes e colocar o Worker na frente.
Como o Worker funciona
- Ele mantém um cache em memória chamado
serverHealth, com:healthy: último status conhecidolastCheck: quando foi o último check
- A cada request:
- Ele atualiza o health check se passou do
HEALTH_CHECK_INTERVAL. - Ele escolhe o primeiro servidor saudável.
- Se nenhum estiver saudável, ele cai no primeiro como fallback.
- Ele atualiza o health check se passou do
- Ele injeta o header
X-Served-Bypra debug. - Se o
fetchpro servidor escolhido der erro, ele tenta 1 vez no outro servidor.
Detalhe importante: esse cache fica na memória do isolate do Worker. Ou seja, ele pode resetar em cold start e não é um “cache global” garantido. Pra failover simples, costuma ser suficiente.
O código
Troque as URLs pelos seus túneis.
// Cloudflare Worker Load Balancer com Health Check
// Configure suas URLs dos túneis aqui
const SERVERS = [
{
url: 'https://seu-tunel-1.seudominio.com',
name: 'Server 1',
healthCheckPath: '/health' // ou '/' se não tiver endpoint específico
},
{
url: 'https://seu-tunel-2.seudominio.com',
name: 'Server 2',
healthCheckPath: '/health'
}
]
// Configurações
const HEALTH_CHECK_TIMEOUT = 5000 // 5 segundos
const HEALTH_CHECK_INTERVAL = 30000 // Checar a cada 30 segundos
// Cache de status dos servidores (mantido pelo Worker)
let serverHealth = {}
// Função para fazer health check
async function checkHealth(server) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT)
const response = await fetch(server.url + server.healthCheckPath, {
method: 'GET',
signal: controller.signal,
headers: {
'User-Agent': 'Cloudflare-Worker-HealthCheck'
}
})
clearTimeout(timeoutId)
// Considera saudável se retornar 2xx ou 3xx
return response.status >= 200 && response.status < 400
} catch (error) {
console.log(`Health check failed for ${server.name}:`, error.message)
return false
}
}
// Função para obter servidor disponível
async function getAvailableServer() {
// Atualiza health check se necessário
for (const server of SERVERS) {
const lastCheck = serverHealth[server.url]?.lastCheck || 0
const now = Date.now()
if (now - lastCheck > HEALTH_CHECK_INTERVAL) {
const isHealthy = await checkHealth(server)
serverHealth[server.url] = {
healthy: isHealthy,
lastCheck: now
}
}
}
// Encontra servidor saudável
for (const server of SERVERS) {
if (serverHealth[server.url]?.healthy) {
return server
}
}
// Se nenhum estiver saudável, tenta o primeiro como fallback
console.log('Nenhum servidor saudável encontrado, usando fallback')
return SERVERS[0]
}
// Handler principal
export default {
async fetch(request, env, ctx) {
// Pega servidor disponível
const server = await getAvailableServer()
// Cria nova URL mantendo o path original
const url = new URL(request.url)
const targetUrl = new URL(url.pathname + url.search, server.url)
// Clona a requisição para o servidor escolhido
const modifiedRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'follow'
})
// Adiciona header para debug (opcional)
modifiedRequest.headers.set('X-Served-By', server.name)
try {
// Faz a requisição para o servidor
const response = await fetch(modifiedRequest)
// Clona a resposta para adicionar headers
const newResponse = new Response(response.body, response)
newResponse.headers.set('X-Served-By', server.name)
return newResponse
} catch (error) {
// Se der erro, tenta o outro servidor
console.log(`Erro ao acessar ${server.name}, tentando fallback`)
const fallbackServer = SERVERS.find((s) => s.url !== server.url)
if (fallbackServer) {
const fallbackUrl = new URL(url.pathname + url.search, fallbackServer.url)
const fallbackRequest = new Request(fallbackUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: 'follow'
})
return fetch(fallbackRequest)
}
return new Response('Todos os servidores estão indisponíveis', { status: 503 })
}
}
}
Como configurar (passo a passo)
-
Crie dois hostnames de túnel
- Túnel do principal:
service-primary.seudominio.com - Túnel da réplica:
service-replica.seudominio.com
- Túnel do principal:
-
Crie o Worker
- Cloudflare Dashboard
- Workers & Pages
- Create Worker
- Cole o código e atualize a lista
SERVERS
-
Aponte o hostname público pro Worker
Crie uma rota (ou custom domain) pra que
service.seudominio.com/*seja atendido por esse Worker. -
Testar qual servidor está respondendo
curl -I https://service.seudominio.comProcure pelo header
X-Served-By. -
Testar o failover
Derrube o tunnel do principal por alguns segundos e faça a request de novo. O header deve mudar pra réplica.
Observações e limitações
- Serviços com estado: se o seu serviço grava estado em disco (sessões, upload, histórico de chat, etc.), pense em storage compartilhado ou sincronização entre os nós.
- Endpoint de health check: prefira um endpoint bem leve (ou
/) e evite checks “caros”. - WebSockets / conexões longas: dependendo do plano e do comportamento do seu app, conexões longas podem exigir ajustes.
- Escopo do cache:
serverHealthfica em memória do Worker e pode resetar. Se você precisar de algo mais consistente, dá pra evoluir usando KV, Durable Objects ou monitoramento externo.
Outra forma de fazer isso
Você pode/consegue usar o mesmo Tunnel em dois servidores/serviços, então basicamente, você poderia ter o Tunnel chamado “Serviços” e conectar ele no “Servidor A” e no “Servidor B”. Algumas pessoas acham essa solução melhor, e em termos de cimplicidade, realmente ele é melhor, mas se você quiser separar os Tunnels, até pra ter mais organização, o Worker Load Balancer é melhor.
Mas caso você ainda queira um Tunnel em todos os seus servidores, segue o que eu percebi por que 1 Tunnel em TODAS as máquinas é uma MÁ IDEIA: Se você usar 1 Tunnel compartilhado em todas as máquinas:
┌─────────────────┐
│ Tunnel "main" │
└─────────────────┘
│
┌───────────┼───────────┬───────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Server A │ │Server B │ │Server C │ │Server D │
│ │ │ │ │ │ │ │
│Jellyfin │ │Navidrome│ │qBittor. │ │OpenWebUI│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
Problema: Cloudflare vai rotear jellyfin.dominio.com para QUALQUER servidor (A, B, C ou D aleatoriamente), mas o Jellyfin só está no servidor A! 💥 Resultado:
- 75% das requisições vão dar erro 502 (caíram no servidor errado)
- Você perde controle de onde cada serviço roda
- Bagunça total
Funciona mas não é recomendado.
Nesse caso, O JEITO CERTO de usar replicas com um único Tunnel seria UM Tunnel compartilhando APENAS quando o MESMO serviço que está em MÚLTIPLAS máquinas:
Cenário 1: Você quer failover do Open WebUI
┌─────────────────────┐
│ Tunnel "openwebui" │
└─────────────────────┘
│
┌────┴────┐
▼ ▼
┌─────────┐ ┌─────────┐
│Server A │ │Server B │
│ │ │ │ ← MESMO Open WebUI (PostgreSQL+Redis compartilhados)
│OpenWebUI│ │OpenWebUI│
└─────────┘ └─────────┘
Funciona, porque o Open WebUI está nos 2 servidores.
Cenário 2: Jellyfin só está em 1 servidor
┌──────────────────┐
│ Tunnel "media" │
└──────────────────┘
│
▼
┌─────────┐
│Server A │
│ │
│Jellyfin │ ← Só aqui
└─────────┘
✅ Correto! Tunnel dedicado porque Jellyfin só está em 1 lugar.
Resumo:
- 1 tunnel por máquina: Serviços diferentes em máquinas diferentes
- 1 tunnel em múltiplas máquinas: MESMO serviço replicado (failover)
- 1 tunnel em todas as máquinas: NUNCA (a não ser que TODOS os serviços estejam em TODAS as máquinas)
Lembre-te amigável
Não se esqueça que você precisa ter os dados sendo compartilhados e acessíveis por todos os servidores, caso contrário você vai ter inconsistências nos dados. E até configurações divergentes, ou ter que configurar cada um de forma diferente.