Um redirecionamento é um bloqueio síncrono. O utilizador clica no seu link curto, o browser para, e nada mais acontece até que o 302 chegue e o próximo carregamento de página possa começar. O redirecionamento não é uma tarefa de fundo que pode desprioritizar. Cada milissegundo que adiciona aqui é um milissegundo subtraído à página que realmente importa.
É por isso que definimos um orçamento rigoroso antes de escrever a primeira linha de services/edge-redirect: p50 5ms, p95 15ms num hit de cache, medido no POP, excluindo o handshake TLS completo. Não aspiracional. Se algo nos empurrar para além da linha, é removido ou movido para um caminho assíncrono.
Temos vindo a executar três regiões de produção - Frankfurt (FRA), Ashburn (ASH) e Singapura (SGP) - durante vários meses. Este artigo é um relato completo de como funciona o caminho crítico, por que os números são como são e o que fizemos mal na primeira vez.
Resumo#
- O caminho crítico é Go + fasthttp no Hetzner FRA/ASH e OVH SGP, atrás do Caddy com encaminhamento anycast. Sem scoring de bots síncrono, sem JS challenge no caminho de redirecionamento.
- Cache de dois níveis: LRU ristretto em processo (L1, ~88% taxa de hit) suportado por Redis Cluster (L1+L2 combinados ~99,4%). gRPC de origem para
api-coreapenas em cold miss (~0,6% dos pedidos). - p95 por região em 90 dias: FRA 12,1ms, ASH 13,4ms, SGP 14,2ms. Cold miss adiciona ~22ms no p95, ainda dentro do orçamento.
- A invalidação de cache em mutação de link é Redis pub/sub, propagação sub-segundo no p99. O TTL do L1 é 60 segundos como rede de segurança.
Por que um teto de 15ms#
Antes de entrar na arquitetura: por que 15ms e não 50ms ou 5ms?
O limite inferior de 5ms é simples - é aproximadamente o que o trânsito físico de rede custa na mediana para um visitante europeu a atingir um POP em Frankfurt. Não se pode ir abaixo da física. O teto de 50ms é demasiado laxo - a 50ms no p95, está a adicionar uma espera percetível antes de cada página para uma fração significativa do seu tráfego. A investigação sobre desempenho web mostra consistentemente que atrasos de rede abaixo de 50ms começam a ser percetíveis em dispositivos móveis onde a latência de rádio se combina com o tempo de processamento, um ponto que as diretrizes de programação network-aware da Apple referem explicitamente.
O número de 15ms surgiu de algumas restrições concretas. Primeiro, os redirecionamentos compõem-se. Se uma campanha de marketing envia tráfego através de um link encurtado que depois redireciona para uma página de produto, a latência do redirecionamento adiciona-se ao TTFB da página de destino. As Core Web Vitals do Google usam o LCP como sinal principal, e uma cadeia de redirecionamentos que adiciona 50ms no p95 é mensurável. Segundo, queremos margem de orçamento suficiente para executar a avaliação de regras para smart links inline no caminho crítico - as dimensões de encaminhamento (país, dispositivo, SO, idioma, hora, referrer) precisam de executar dentro do mesmo envelope de latência que um redirecionamento simples, ou teríamos de remover o suporte a smart links do edge. A 15ms com um custo de avaliação de regras de ~0,3ms, há espaço.
O orçamento de 15ms aplica-se ao tráfego em hit de cache. Os cold misses têm permissão para ser mais lentos - a chamada gRPC de origem adiciona latência - mas os cold misses por design são raros o suficiente para não moverem significativamente o p95.
A arquitetura#
Três POPs, cada um com o mesmo binário: services/edge-redirect, escrito em Go usando fasthttp. O throughput do servidor fasthttp é aproximadamente 8x o net/http no suite de benchmarks e, mais praticamente para nós, o seu caminho de pedido zero-alloc mantém as pausas de GC previsíveis sob carga sustentada. O net/http da biblioteca padrão está bem para a maioria dos serviços; para um handler de redirecionamento que precisa de manter sub-milissegundo de tempo de processamento com alta concorrência, evitar alocação de heap por pedido vale a pena a API menos ergonómica.
O Caddy está à frente como terminador TLS e proxy reverso. TLS on-demand para domínios personalizados de tenants (descrito em detalhe na página de funcionalidade de domínios personalizados) provisiona certificados no primeiro pedido. Avaliámos o HAProxy e o nginx como alternativas - ambos são rápidos, ambos têm padrões maduros de implementação anycast, mas o TLS on-demand do Caddy é o caminho mais limpo para o ciclo de vida de certificados zero-touch para um número arbitrário de domínios de clientes, e isso importa-nos mais do que espremer mais uma fração de milissegundo na camada de proxy.
O encaminhamento anycast significa que quando um visitante acede a f.elido.me, s.elido.me ou b.elido.me, o DNS resolve para um prefixo anycast partilhado e a rede encaminha a ligação TCP para o POP mais próximo. Não há lógica de geo-roteamento ao nível da aplicação: a rede faz a seleção do POP. O primer anycast da Cloudflare é a explicação pública mais clara de por que isso importa - a propriedade fundamental é que o failover é tratado na camada BGP, não pela expiração do TTL DNS. Se o FRA perder conectividade, o ASH torna-se o caminho mais curto para o tráfego europeu em segundos, não minutos. A documentação da infraestrutura de rede cloud do Hetzner cobre a configuração de encaminhamento subjacente para as suas regiões FRA e ASH.
Importante: não há scoring de bots síncrono no caminho crítico. Uma verificação de scoring de bots que leve 10ms destruiria sozinha o orçamento de p95. Todos os sinais de qualidade de tráfego - deteção de anonimizadores, scoring de ASN de hosting, deduplicação de cliques - correm no url-scanner e click-ingester como workers assíncronos de caminho frio. O redirecionamento dispara e o clique vai para a fila do Redpanda; a adjudicação de qualidade acontece a posteriori.
O cache de dois níveis#
O cache é onde vive o orçamento. A lógica:
// Simplified cache lookup: L1 → L2 → origin, with singleflight dedup
func (h *RedirectHandler) resolve(ctx *fasthttp.RequestCtx, slug string) (*Link, error) {
// L1: in-process ristretto LRU - sub-microsecond on hit
if link, ok := h.l1.Get(slug); ok {
return link.(*Link), nil
}
// L2 + origin share a singleflight group to prevent thundering herd
// on concurrent cold misses for the same slug
val, err, _ := h.sf.Do(slug, func() (interface{}, error) {
// L2: Redis Cluster - single RTT, typically 0.3–0.8ms within POP
if data, err := h.redis.Get(ctx, cacheKey(slug)).Bytes(); err == nil {
link, err := unmarshalLink(data)
if err == nil {
h.l1.Set(slug, link, linkCost(link))
return link, nil
}
}
// Origin: gRPC to api-core - cold miss, ~20ms extra
link, err := h.origin.GetLink(ctx, &pb.GetLinkRequest{Slug: slug})
if err != nil {
return nil, err
}
payload, _ := marshalLink(link)
h.redis.Set(ctx, cacheKey(slug), payload, redisTTL)
h.l1.Set(slug, link, linkCost(link))
return link, nil
})
if err != nil {
return nil, err
}
return val.(*Link), nil
}
O L1 é o ristretto, o cache LRU com controlo de admissão da Dgraph. O controlador de admissão importa: um LRU ingénuo sob uma carga de scan (um bot a aceder a milhares de slugs únicos) irá despejar entradas quentes para dar lugar a entradas frias que nunca serão pedidas novamente. A política de admissão baseada em TinyLFU do ristretto resiste a isto - rastreia contadores de frequência de forma barata e recusa admitir uma entrada que nunca foi vista antes quando o cache está sob pressão. O efeito líquido é que a taxa de hit do cache sob tráfego de scan adversarial se mantém perto da taxa de hit orgânica em vez de colapsar.
O L2 é o Redis Cluster. Cada POP tem a sua própria instância de cluster para manter o tráfego inter-regiões fora do caminho crítico. FRA e ASH partilham uma instância Redis separada para sinais de invalidação pub/sub (mais sobre isso abaixo); SGP tem a sua. Um único GET Redis dentro do mesmo datacenter é fiável abaixo de 1ms. A taxa de hit combinada L1+L2 está em aproximadamente 99,4% nos últimos 90 dias - o que significa que as chamadas de origem acontecem em cerca de 1 em 167 pedidos.
Para o caso de uso de solutions/developers - equipas que usam a API para cunhar links em alto volume - a implicação prática é que um link recém-criado experienciará um cold miss por POP, depois ficará quente pelo tempo do seu TTL. Os links que não têm tráfego expiram de ambos os caches limpamente sem evicção manual.
Para onde vão os 15ms#
O diagrama abaixo desagrega o orçamento de hit de cache p95 por fase:
O segmento dominante é o retorno de rede - aproximadamente 9ms mediana, o que significa que a distância física entre o visitante e o POP representa 60% do orçamento. Não conseguimos comprimir isto. A implementação multi-região é a única alavanca: adicionar um POP reduz o RTT mediano para os visitantes nessa região. A próxima região no roadmap reduz o p95 do SGP para o tráfego do Sul Asiático, onde atualmente estamos a encaminhar 14ms porque Singapura é o POP mais próximo.
A retoma de sessão TLS a 2ms assume TLS 1.3 0-RTT com um ticket de sessão já disponível. Para uma primeira visita a partir de um dado dispositivo, um handshake TLS completo adiciona aproximadamente 10-15ms por cima - é por isso que o orçamento de 15ms se aplica explicitamente ao tráfego de hit de cache + sessão retomada, que é a grande maioria do tráfego de cliques na prática. O RFC 7234 rege a semântica de caching para a camada HTTP; note-se que as respostas 302 não são armazenadas pelos caches do browser por padrão (§4.2.2), que é o comportamento correto para o nosso caso de uso - cada pedido de redirecionamento chega ao edge, cada redirecionamento recebe a sua própria decisão de encaminhamento, sem destino desatualizado no cache do browser.
A margem de 2,6ms é espaço operacional real, não padding. Sob o GC do Go, pausas stop-the-world ocasionais da ordem de 0,5-1ms são esperadas mesmo com configurações GOGC ajustadas. O overhead de proxy do Caddy adiciona um pequeno custo fixo. A margem evita que ultrapassemos o orçamento quando estes efeitos se compõem.
Invalidação de cache#
O Redis pub/sub é o mecanismo. Quando um link é mutado no api-core - destino alterado, regras de targeting atualizadas, link arquivado - o handler de mutação publica para um canal link:invalidate com o slug como payload. Cada POP edge subscreve este canal. Na receção, o subscritor chama l1.Del(slug) e redis.Del(cacheKey(slug)). O próximo pedido para esse slug repopula ambos os níveis a partir da origem.
O TTL de 60 segundos do L1 é o fallback, não o mecanismo principal. Se o subscritor pub/sub estiver inativo - digamos, uma falha Redis ou uma partição de rede entre o POP e a instância pub/sub - a entrada expira do L1 em no máximo 60 segundos. O TTL do L2 é definido para 300 segundos, por isso uma interrupção do subscritor significa até 5 minutos de dados L2 potencialmente desatualizados, durante os quais o TTL do L1 é a única rede de segurança. Alertamos para perda de subscrição pub/sub dentro de 30 segundos.
Para smart links com regras com janelas temporais, a obsolescência tem uma implicação específica: se uma regra ativar às 17:00 e o L1 do POP edge tiver a versão anterior da regra em cache com até 60 segundos de TTL restante, o tráfego entre 17:00 e 17:01 pode ir para o destino anterior à atualização. O caminho pub/sub elimina isto no caso comum; o TTL de 60 segundos apanha o caso de extremo. Para campanhas onde o limite temporal importa precisamente, o padrão recomendado é usar status=disabled na regra antiga, aguardar um ciclo TTL (60 segundos), depois ativar a nova. Adicionámos um endpoint de polling em GET /v1/links/{id}/cache-status para que os pipelines possam confirmar a propagação antes de prosseguir.
Medições por região reais#
Os seguintes números provêm de dados de workspace de demonstração recolhidos durante 90 dias até 2026-05-12. Refletem apenas tráfego em hit de cache. Todos os timestamps são UTC.
| Região | POP | p50 | p95 | p99 |
|---|---|---|---|---|
| EU (Frankfurt) | FRA · Hetzner | 4,8ms | 12,1ms | 18,4ms |
| US East (Ashburn) | ASH · Hetzner | 5,2ms | 13,4ms | 20,1ms |
| SE Asia (Singapura) | SGP · OVH | 5,6ms | 14,2ms | 22,8ms |
O FRA é o mais rápido porque a maioria da carga é europeia, pelo que o RTT mediano é menor. O SGP serve uma distribuição geográfica mais ampla - o tráfego do Sudeste Asiático tem RTT mais baixo, enquanto o tráfego do Sul Asiático e do Leste Asiático adiciona-se à cauda.
Os números p99 excedem o orçamento de 15ms. Isso é deliberado. O p95 é o orçamento, não o p99. O p99 é moldado por condições extremas: transições celulares, retransmissões TCP, o ocasional pico de latência Redis. Monitoramos o p99 mas não o incluímos no SLA. A decisão de engenharia é que o p95 captura a experiência para "quase todos quase sempre", e otimizar os últimos 1% exigiria eliminar fontes de variabilidade natural de rede que não estão sob o nosso controlo.
O p95 do cold miss é aproximadamente 22ms. Este é o mínimo que podemos alcançar dado que o gRPC de origem adiciona uma viagem de ida e volta no mesmo datacenter (FRA → FRA por rede privada é aproximadamente 0,3ms) mais o lookup Postgres do api-core (tipicamente 1-3ms para um lookup de slug por chave). O número de 22ms é medido, não estimado; está dentro do orçamento que permitimos para caminhos de cold miss, que está definido em 35ms no p95.
Para equipas a avaliar análises multi-região, estes números de latência estão disponíveis como métrica Prometheus (redirect_duration_seconds com rótulos region e cache_tier) a partir do endpoint de métricas.
Modos de falha sobre os quais não escrevemos da primeira vez#
Thundering herd na expiração de chave#
Antes de adicionarmos o singleflight, um slug a expirar de L1 e L2 simultaneamente sob tráfego moderado gerava uma rajada de chamadas gRPC de origem concorrentes - cada uma a fazer um leitura Postgres para o mesmo slug, todas a devolver o mesmo resultado. Sob teste de carga, isto produzia picos de CPU no api-core que não tinham nada a ver com o volume de criação de links. O grupo singleflight colapsa misses concorrentes para o mesmo slug numa única chamada de origem. As outras goroutines em espera bloqueiam no grupo e recebem o mesmo resultado quando ele resolve. A implementação é o pacote padrão Go golang.org/x/sync/singleflight.
Acertámos nisto no primeiro protótipo. Um thundering herd sob expiração de chave é um dos modos de falha que não aparecem em testes unitários - só surge sob concorrência realista. A incluir neste artigo porque é uma omissão comum em writeups de arquitetura de cache e a correção é genuinamente simples.
Fallback em falha Redis#
Se um POP perder conectividade com o seu cluster Redis, o fallback não é um erro - o caminho de código degrada para apenas L1 mais gRPC de origem direto em miss de L1. O POP continua a servir. A taxa de hit cai porque L2 está indisponível, pelo que o volume de chamadas de origem sobe, mas o caminho de redirecionamento mantém-se funcional. O caminho de falha Redis foi exercitado duas vezes em produção (ambas foram janelas de manutenção do Hetzner). A taxa de chamadas de origem de pico durante o segundo incidente foi aproximadamente 8x a linha de base pelo tempo da falha (~4 minutos). O api-core tratou disso sem eventos de escalonamento.
Propagação DNS durante failover de POP#
O failover anycast é na camada BGP - sem TTL DNS a aguardar, sem timeout de verificação de saúde ao nível da aplicação no caminho do pedido. Um POP a ficar offline desencadeia a retirada BGP da rota, e o tráfego de rede muda para o POP mais próximo seguinte dentro da janela de convergência BGP (tipicamente 15-90 segundos dependendo do número de saltos de rede para o caminho afetado). O parâmetro operacional relevante é o nosso intervalo de verificação de saúde: executamos verificações TCP de saúde a cada 10 segundos por POP. Uma falha de verificação desencadeia a retirada. Um intervalo de verificação de 10 segundos significa que um POP em colapso pode servir até 10 segundos de tráfego falhado antes da retirada. Testámos este limite deliberadamente; o impacto real nos dois incidentes de produção foi abaixo do intervalo de verificação.
O que não fazemos no caminho crítico#
Cada item que não está no caminho crítico é uma escolha deliberada, não uma omissão.
Escritas síncronas de cliques. Os cliques são fire-and-forget para o Redpanda. O handler de redirecionamento acrescenta um evento de clique a um tópico Kafka (clicks.raw) com o slug, timestamp, IP truncado e hash de user-agent, depois responde com o 302. A escrita é não-bloqueante. Se o Redpanda estiver indisponível, o clique é descartado - não o redirecionamento. Fizemos a troca consciente de que a perda de cliques sob falha de infraestrutura é aceitável e a falha de redirecionamento não é. O consumidor click-ingester processa o tópico Redpanda e escreve para o ClickHouse. É por isso que os dados de análises para um dado evento de clique estão disponíveis com um pequeno atraso (tipicamente abaixo de 5 segundos), não instantaneamente.
Desafios de bots inline. Um desafio de bot adiciona 10-50ms de trabalho síncrono no mínimo - os desafios JavaScript adicionam uma viagem de ida e volta completa. Não fazemos nenhum dos dois no caminho de redirecionamento. O serviço url-scanner processa sinais de qualidade de tráfego de forma assíncrona. Para equipas de solutions/developers a construir campanhas de links, isto significa que o redirecionamento nunca é bloqueado atrás de um desafio que degrada a experiência para o tráfego legítimo.
Validação de esquema no momento do redirecionamento. O URL de destino e as regras de targeting são validados no momento da escrita, quando o link é criado ou atualizado via api-core. No momento em que um slug entra no cache, a sua estrutura é conhecida como válida. Não há validação de esquema JSON, passo de parse de URL, nem verificação de sintaxe de regras no momento do redirecionamento. O binário edge confia completamente na entrada do cache. Isto só é seguro porque o caminho de escrita valida antes da admissão ao cache.
As partes sem glamour#
Três coisas sobre as quais não escrevemos suficientemente, porque são entediantes de ler e importantes de acertar.
Orçamentos de tamanho de cache. O ristretto é inicializado com um orçamento de custo explícito em bytes, não uma contagem simples de itens. Cada link em cache tem um custo pelo seu tamanho serializado, que varia com o número de regras de targeting. Um link sem regras custa aproximadamente 200 bytes; um link com 6 regras de targeting custa cerca de 800 bytes. O orçamento é definido para consumir no máximo 10% da RAM disponível da instância, deixando espaço para o runtime Go, o Caddy e os buffers de ligação. Acertar nisto errado causa thrashing de cache: um orçamento demasiado pequeno despeja entradas antes de o TTL expirar, empurrando tráfego para L2 e origem.
Ajuste de GC sob carga. O coletor de lixo do Go está bem ajustado por padrão, mas o GOGC=100 padrão desencadeia GC a duas vezes o tamanho do heap ativo. Para um handler de redirecionamento onde o heap ativo é pequeno mas a taxa de alocação é moderada (o fasthttp é zero-alloc no caminho crítico, mas há alocações de objetos para eventos de clique e chamadas gRPC), o GC dispara com mais frequência do que necessário. Executamos GOGC=400 em produção. O efeito são ciclos GC mais longos mas com menor frequência - que importa para a latência de cauda. Um ciclo GC que leva 2ms e acontece uma vez a cada 4 segundos adiciona uma contribuição menor ao p99 do que um ciclo de 1ms por segundo. Verificámos isto empiricamente com make bench antes de o definir na configuração de implementação.
A disciplina make bench. O binário edge tem um suite de benchmarks (go test -bench=. -benchmem ./... a partir de services/edge-redirect). Cada alteração proposta ao caminho crítico - adicionar um novo cabeçalho, alterar o formato da chave de cache, ajustar o avaliador de regras - corre pelos benchmarks antes do merge. Uma alteração que adiciona 0,5ms ao benchmark p50 é uma alteração que move o p95 em produção. O benchmark é o gate, não uma verificação post-hoc. Uma vez ficámos relaxados sobre isto, numa refatoração que alterou a lógica de normalização de slugs, e lançámos uma regressão de 1,2ms que apareceu nos dashboards de região dois dias depois. A regressão foi real e a lição ficou.
As decisões de arquitetura aqui estão documentadas em mais detalhe em /docs/architecture/edge-redirect. Se estiver a avaliar o Elido como camada de infraestrutura de redirecionamento para uma campanha de alto volume ou uma plataforma de programadores, a página solutions/developers cobre a superfície da API e as opções de SDK. Para uma análise do que o cache de dois níveis implica para o comportamento de smart links - particularmente a janela de propagação para alterações de regras - o artigo smart links explained cobre isso em profundidade.
Marius Voß é DevRel e edge infra no Elido. Foi um dos engenheiros que levou o binário edge-redirect do protótipo à produção e tem vindo a olhar para os seus dashboards de latência desde então.
Experimente Elido
Cole uma URL, obtenha um link curto
Sem cadastro. O link vive 30 dias. Cadastre-se para mantê-lo para sempre.
Grátis, sem necessidade de registo · 2 por dia