Por Flank Manoel da Silva Especialista Sênior Full Stack | Analista de Infraestrutura e Performance
Existe um momento específico na vida de qualquer time de engenharia que trabalha com distribuição de conteúdo em escala: o instante em que alguém percebe que o botão “purge all” da CDN acabou de acionar uma crise, não resolvido uma. O preço de uma promoção relâmpago foi atualizado no banco de dados. O operador executou a purga. Quarenta e dois segundos depois, usuários em São Paulo ainda viam R$ 199,00. Usuários em Frankfurt já viam R$ 159,00. E o origin server estava respondendo como se tivesse entrado em colapso, porque literalmente todos os edge nodes do planeta decidiram revalidar ao mesmo tempo.
Esse cenário não é exceção. É a regra. E a razão pela qual ele se repete é que a narrativa dominante sobre CDN, “coloque na beira, sirva rápido, escale sem dor”, omite completamente a parte mais difícil: o que acontece quando você precisa desfazer o que foi armazenado.
A verdade oculta, que documentações de fornecedores raramente colocam em destaque, é que em aplicações dinâmicas, o custo total de invalidação supera frequentemente o custo de servir o conteúdo em si. Esse custo não é apenas financeiro. Ele se manifesta como inconsistência temporal entre regiões, como picos de carga no origin, como experiências divergentes para usuários diferentes no mesmo segundo, e como dívida operacional que cresce cada vez que alguém adiciona um novo edge node ao pool.
O que os benchmarks de purge não contam a você
A Cloudflare publicou em setembro de 2024 números que são genuinamente impressionantes: latência de purge global de 149ms no P50, contra 1.570ms em maio de 2022 uma melhoria de 90,5% alcançada com a migração para uma arquitetura distribuída sem core central, usando RocksDB como storage engine e um protocolo peer-to-peer direto entre edge nodes.
| Região | P50 Ago/2024 (Coreless) | P50 Mai/2022 (Core-based) | Melhoria |
|---|---|---|---|
| África | 303ms | 1.420ms | 78,66% |
| APAC | 199ms | 1.300ms | 84,69% |
| Leste Europeu | 140ms | 1.240ms | 88,70% |
| Leste América do Norte | 119ms | 1.080ms | 88,98% |
| Oceania | 191ms | 1.160ms | 83,53% |
| América do Sul | 196ms | 1.250ms | 84,32% |
| Oeste Europeu | 131ms | 1.190ms | 88,99% |
| Oeste América do Norte | 115ms | 1.000ms | 88,50% |
| Global | 149ms | 1.570ms | 90,5% |
Esses números são o P50. Metade das purgas demora menos. A outra metade demora mais e dependendo da região, o comportamento de cauda longa (P95, P99) pode ser dramaticamente diferente. O CDN Planet, que mede empiricamente o tempo de purge por CDN a partir de três localizações geográficas distintas (EUA, Reino Unido e Hong Kong), revela outra dimensão do problema: a consistência entre regiões. Uma purge confirmada pela API não é equivalente a uma purge concluída em todos os edge nodes simultaneamente.
O problema começa exatamente aí. Quando você envia um request de purge, a API responde com 200 OK em milissegundos. Mas esse 200 OK significa apenas que a instrução foi aceita, não que foi executada em todos os pontos de presença. O intervalo entre a confirmação da API e a conclusão real da invalidação em cada edge node é o que chamamos de janela de inconsistência temporal e é nessa janela que as coisas desmoronam em produção.
A janela de inconsistência temporal: o problema real
Imagine o seguinte fluxo:
t=0ms → Purge request enviado para API da CDN
t=12ms → API responde 200 OK
t=15ms → Edge nodes em Western North America invalidam
t=131ms → Edge nodes em Western Europe invalidam
t=196ms → Edge nodes em América do Sul invalidam
t=303ms → Edge nodes em África invalidam
Durante os 303ms entre o primeiro e o último edge node processar a invalidação, qualquer usuário que fizer uma request terá uma experiência determinada pelo acaso geográfico. Um usuário em Londres e um usuário em Johannesburgo, fazendo requests simultâneos, receberão versões diferentes do mesmo recurso. Isso é a definição operacional de inconsistência de cache em sistemas distribuídos.
Para conteúdo estático, um logo redesenhado, um arquivo CSS atualizado, essa janela de 303ms é irrelevante. Para preços de produtos, níveis de estoque, configurações de segurança ou qualquer dado com implicações de negócio, ela pode ser fatal.
O custo verdadeiro de invalidar: três dimensões que todo engenheiro deveria medir
- Dimensão 1: o custo financeiro direto
A AWS CloudFront pratica um modelo aparentemente generoso: as primeiras 1.000 invalidation paths por mês são gratuitas, e o excedente custa US$ 0,005 por path. O detalhe que passa despercebido é que um wildcard como /* conta como um único path, parecendo uma vantagem até você perceber que ele invalida tudo de uma vez, sem granularidade, ativando o problema do thundering herd.
Para times que operam plataformas de e-commerce com catálogos dinâmicos, a conta muda de figura. Um sistema que invalida 50 objetos individuais por evento de atualização de preço, com 300 eventos diários, gera 15.000 invalidações por dia, 450.000 por mês. Descontadas as primeiras 1.000 gratuitas, isso representa US$ 2.245/mês apenas em invalidações no CloudFront. Não em tráfego. Não em origem. Apenas no ato de dizer à CDN que o conteúdo mudou.
Fornecedores como Fastly e Cloudflare (em planos superiores) têm modelos diferentes, geralmente baseados em volume de purge requests por segundo em vez de por path. Mas o princípio não muda: invalidação tem custo, e esse custo cresce proporcionalmente à dinamicidade da aplicação.
- Dimensão 2: o custo de latência no origin: o thundering herd
O thundering herd (ou cache stampede) é o efeito colateral mais devastador de uma purge mal planejada. O mecanismo é simples: quando você invalida uma chave de cache popular, todos os requests que chegam antes da revalidação completar são encaminhados ao origin server. Se a chave era responsável por absorver 10.000 requests por segundo, o origin recebe 10.000 requests simultâneos em um intervalo de milissegundos.
Em plataformas com tráfego concentrado, uma Black Friday, um evento ao vivo, uma promoção relâmpago que começa ao meio-dia, esse spike pode ser suficiente para derrubar o origin ou causar timeouts em cascata. O pior cenário possível é uma purge disparada precisamente no momento de maior tráfego, que é exatamente quando times de operações tendem a executar atualizações de emergência.
A ironia técnica aqui é elegante na sua crueldade: você invalida o cache para garantir que o conteúdo correto seja entregue mais rápido. O processo de invalidação garante que, durante alguns segundos, o conteúdo seja entregue mais devagar para todos, ou não seja entregue para ninguém.
- Dimensão 3: o custo de coordenação em sistemas multi-camada
A maioria das arquiteturas de produção não tem um único nível de cache. O fluxo típico de um request em uma aplicação moderna passa por algo como:
Browser Cache → Service Worker → Edge CDN → Regional PoP (CDN mid-tier) →
Application Cache (Redis/Memcached) → Query Cache (DB) → Origin
Uma purge na CDN invalida apenas um desses níveis. O Redis no origin pode ainda estar servindo dados desatualizados. O browser cache do usuário, com um Cache-Control: max-age=3600, não será afetado de forma alguma, a menos que você também tenha implementado mecanismos de invalidação de browser cache, o que é notoriamente difícil e raramente feito de forma robusta.
Esse problema de coordenação multi-camada transforma a invalidação em um problema de sistemas distribuídos no sentido mais técnico do termo: você tem múltiplos nós, cada um com seu próprio estado, e precisa garantir consistência sem um coordenador central que possa criar gargalos. Phil Karlton não estava exagerando quando disse que cache invalidation é um dos dois problemas mais difíceis da ciência da computação.
Cenário A vs. Cenário B: quando a mesma estratégia funciona e quando quebra
Cenário A — plataforma editorial com conteúdo de longa vida
Uma publicação de notícias serve artigos que, uma vez publicados, raramente mudam. Quando mudam, a atualização pode tolerar uma janela de inconsistência de alguns minutos sem consequências de negócio. Nesse cenário, uma CDN com TTL longo (s-maxage=3600) e purge por URL individual funciona bem. O custo de invalidação é baixo porque os eventos de invalidação são raros. O thundering herd é mitigado naturalmente pelo fato de que artigos individuais têm volumes de tráfego moderados e previsíveis.
A ferramenta de purge aqui é um instrumento cirúrgico. Você a usa com precisão, em casos específicos, e ela cumpre exatamente o que promete.
Cenário B — plataforma de e-commerce com estoque em tempo real
Um marketplace com 500.000 SKUs ativos, em que preços e disponibilidade podem mudar a cada transação. O time de engenharia tentou implementar TTLs curtos (s-maxage=30) para garantir frescor. Resultado: cache hit ratio despencou para 40%, o origin começou a receber o dobro do tráfego esperado, e o custo de infraestrutura subiu 60% em três meses.
A solução óbvia parece ser purge por evento, quando o preço muda, invalida aquela URL específica. Mas um produto que aparece em 47 páginas de categoria, 12 resultados de busca cached, e 8 widgets de recomendação personalizada não tem uma URL única. Tem 67 URLs que precisam ser invalidadas em coordenação.
Nesse cenário, a purge por URL individual não é cirurgia. É amputação com tesoura de cozinha.
Surrogate Keys e Cache Tags: a arquitetura que muda o jogo
A resposta para o Cenário B existe, mas requer repensar a arquitetura desde a camada de resposta HTTP. Surrogate Keys (nome dado pelo Fastly) ou Cache Tags (terminologia usada pela Cloudflare, Akamai e Varnish) são cabeçalhos de resposta que associam um objeto em cache a múltiplos identificadores semânticos ao mesmo tempo.
Em vez de cachear /produto/123456 e precisar saber manualmente todas as páginas que exibem esse produto, você instrui o origin a responder com:
Surrogate-Key: produto-123456 categoria-eletronicos marca-samsung promo-blackfriday
Quando o produto muda, você dispara uma única purge request pela tag produto-123456. A CDN, internamente, identifica todos os objetos em cache que carregam essa tag e os invalida de forma atômica, incluindo as 67 URLs do exemplo anterior.
O resultado prático é uma inversão de paradigma: em vez de gerenciar invalidação pelo endereço do conteúdo (URL), você gerencia pela semântica do conteúdo (o que ele representa). Isso é especialmente poderoso em arquiteturas que usam componentes compartilhados — um banner de promoção que aparece em centenas de páginas, um bloco de navegação que exibe contagem de categorias, um widget de “mais vendidos” presente em toda página de produto.
Suporte a cache tags por fornecedor
| Fornecedor | Suporte a Cache Tags | Plano Necessário | Granularidade de Purge |
|---|---|---|---|
| Fastly | Nativo (Surrogate-Key) | Todos os planos | Alta — por tag individual |
| Cloudflare | Cache Tags | Enterprise + alguns Business | Alta — por tag, hostname, prefix |
| Akamai | Edge Tags (Fast Purge) | Enterprise | Alta — por CP code, tag, ARL |
| AWS CloudFront | Não nativo | N/A | Baixa — por URL ou wildcard |
| Varnish (self-hosted) | Xkey VMOD / Ykey (Enterprise) | Open source / Commercial | Alta — configurável |
| Azure CDN | Não nativo | N/A | Por path ou wildcard |
A ausência de suporte nativo a cache tags no CloudFront é um dado crítico para times que constroem sistemas dinâmicos na AWS. O workaround mais comum, versioning via query string ou path (/produto/123456?v=20260223), resolve o problema de stale content, mas não resolve o problema de invalidação coordenada. Ele apenas muda stale content por acumulação de versões órfãs em cache.
O teste de propagação: como medir a janela de inconsistência no seu ambiente
Antes de qualquer decisão arquitetural sobre estratégia de invalidação, você precisa saber com precisão qual é a sua janela de inconsistência temporal. Medi-la é mais simples do que parece, e os dados que você obtém são consistentemente surpreendentes.
Protocolo de Teste
A metodologia usada pelo CDN Planet, que medimos e consideramos robusta, envolve:
Fase 1 — Warm-up: garanta que o objeto de teste está cacheado em pelo menos três regiões geográficas distintas. Confirme o HIT através do cabeçalho de status de cache do fornecedor (CF-Cache-Status: HIT, X-Cache: Hit from cloudfront, etc.).
Fase 2 — Disparo: registre o timestamp exato (com precisão de milissegundo) antes de enviar o request de purge. Esse é o PurgeStartTime.
Fase 3 — Polling paralelo: a partir de cada região geográfica de teste, dispare requests em intervalos crescentes:
- Imediatamente após o envio
- A cada 1 segundo por 5 segundos
- A cada 2 segundos por 10 segundos
- A cada 5 segundos até 60 segundos
Fase 4 — Registro: para cada região, registre o timestamp do primeiro request que retornar MISS (ou equivalente). Esse é o PurgeEndTime regional.
Fase 5 — Análise: a diferença entre PurgeEndTime regional e PurgeStartTime é o tempo de purge para aquela região. A diferença entre o PurgeEndTime mais rápido e o mais lento é sua janela máxima de inconsistência.
O que fazer com esses dados
Uma janela de inconsistência de 150ms é operacionalmente irrelevante para quase todos os casos de uso. Uma janela de 8 segundos, sendo o que você encontra com alguma frequência no Azure CDN, segundo relatos de engenheiros em produção na Microsoft Learn, já merece atenção. Uma janela de 45 segundos (o limite máximo que o protocolo do CDN Planet permite detectar) significa que você tem uma aplicação que não pode usar purge como mecanismo primário de frescor de dados.
A métrica que poucos times calculam é o custo por segundo de inconsistência, quantas transações passam pela janela de inconsistência em um segundo de tráfego típico. Para uma plataforma com 500 requisições por segundo em uma página de produto, uma janela de 8 segundos significa 4.000 usuários que podem ter visto um preço desatualizado. Se 2% desses usuários completaram uma compra, isso é 80 pedidos com dados de preço incorretos.
Stale-while-revalidate: a alternativa que transfere o problema
O stale-while-revalidate é frequentemente apresentado como a solução elegante para o dilema entre frescor e desempenho. A diretiva instrui a CDN a servir o conteúdo cacheado imediatamente (independente de estar expirado) enquanto busca uma versão atualizada em background para o próximo request.
Cache-Control: s-maxage=60, stale-while-revalidate=300
Nesse exemplo, o conteúdo é servido do cache por até 60 segundos garantidos. Entre 60 e 360 segundos após o cache, a CDN serve a versão stale ao usuário atual e simultaneamente revalida em background. Após 360 segundos, a CDN bloqueia o request e espera a resposta do origin.
O ganho é real: elimina a latência de revalidação bloqueante para o usuário. Mas o que essa diretiva faz com o problema de consistência é transferi-lo, não resolvê-lo. Com stale-while-revalidate=300, você pode ter cinco minutos de defasagem intencional, não acidental. Para conteúdo onde a atualidade é crítica, você acabou de definir que “desatualizado por cinco minutos” é o comportamento esperado.
A tensão real é entre dois objetivos genuinamente conflitantes:
Objetivo 1: Maximizar a taxa de acerto do cache (cache hit ratio) para reduzir latência e carga no origin.
Objetivo 2: Minimizar a janela de inconsistência para garantir que usuários vejam dados corretos.
Esses objetivos se movem em direções opostas. Aumentar TTL melhora o hit ratio e piora a consistência. Diminuir TTL melhora a consistência e piora o hit ratio. Qualquer engenheiro que diz ter resolvido esse trade-off sem sacrifícios está ou simplificando ou vendendo uma solução específica.
A anatomia de uma invalidação correta em aplicações dinâmicas
Depois de anos observando arquiteturas em produção e os incidentes que surgem quando elas são pressionadas, o padrão que funciona consistentemente não é uma tecnologia específica. É uma abordagem em camadas que reconhece cada nível de cache como um problema separado.
Decidir o que não deve ser cacheado
Antes de qualquer discussão sobre estratégia de invalidação, a pergunta mais importante é: esse conteúdo deveria estar em cache na CDN? Para dados que mudam com frequência maior que o custo de invalidação, a resposta pode ser não. Preços em tempo real, níveis de estoque com granularidade de unidade, dados de sessão do usuário, esses são candidatos naturais para bypass de CDN (Cache-Control: no-store ou CDN-Cache-Control: no-store) combinado com cache de curta duração em camadas mais próximas do origin (Redis, Memcached).
Segmentar conteúdo por volatilidade
Um erro clássico é tratar toda a página como uma unidade atômica de cache. Uma página de produto tem componentes com volatilidades radicalmente diferentes:
| Componente | Volatilidade | TTL Adequado | Estratégia |
|---|---|---|---|
| Template/estrutura HTML | Muito baixa | 24h+ | Cache longo + purge por deploy |
| Imagens do produto | Baixa | 7 dias | Cache longo + versioning |
| Descrição do produto | Baixa-média | 1h | Cache médio + purge por evento |
| Preço atual | Alta | Bypass CDN | Fetch dinâmico via API |
| Disponibilidade em estoque | Muito alta | Bypass CDN | Fetch dinâmico via API ou SSE |
| Reviews e avaliações | Média | 15min | Cache curto + surrogate key |
Essa segmentação, frequentemente implementada via Edge Side Includes (ESI) em Varnish ou via Cloudflare Workers que montam a página a partir de fragmentos com TTLs independentes, permite que o template pesado fique em cache por 24 horas enquanto o preço é buscado dinamicamente a cada request. O resultado é um hit ratio alto para os componentes que representam a maior parte do payload, sem sacrificar a precisão dos dados críticos.
Event-driven invalidation com backpressure
Para os componentes que devem ser cacheados mas precisam de invalidação rápida, o padrão mais robusto é event-driven: em vez de o cliente web disparar purges manualmente, o sistema de backend publica eventos em um message queue (Kafka, SQS, RabbitMQ) quando dados mudam. Um consumidor dedicado recebe esses eventos e dispara as purges correspondentes via API da CDN.
A vantagem arquitetural aqui é o backpressure natural do queue: em um evento de Black Friday com 50.000 atualizações de preço simultâneas, o queue absorve o spike e os purges são processados em ordem, evitando que a API da CDN seja sobrecarregada com requests paralelos que podem resultar em rate limiting ou comportamento imprevisível.
A timeline abaixo ilustra a diferença entre os dois modelos:
MODELO SÍNCRONO (sem queue):
t=0ms → Evento de mudança de preço
t=0ms → Chamada direta à API de purge
t=12ms → API responde (ou falha sob carga)
t=??? → Sem garantia de order, sem retry, sem backpressure
MODELO ASSÍNCRONO (com queue):
t=0ms → Evento de mudança de preço publicado no queue
t=~5ms → Consumidor recebe evento
t=~17ms → Request à API de purge enviado
t=~29ms → API confirma recebimento
t=~180ms → Invalidação concluída globalmente
+ Retry automático em caso de falha
+ Rate limiting natural via throughput do consumidor
+ Observabilidade completa via logs do queue
O que ninguém diz sobre Purge Global vs. Purge por Tag
Existe uma tentação operacional muito humana: quando algo dá errado com o cache, a resposta instintiva é “purga tudo”. O botão purge everything ou /* existe em todas as CDNs, e é usado com uma frequência que deveria preocupar qualquer SRE.
O purge global funciona como esperado em termos de garantia de consistência, após a propagação, todos os edge nodes estão servindo conteúdo fresco. O problema é o que acontece imediatamente depois.
Considere uma CDN servindo 100.000 requisições por segundo em horário de pico, com um cache hit ratio de 85%. Isso significa que o origin está recebendo 15.000 requests por segundo normalmente. Um purge global faz o hit ratio cair para 0% temporariamente. O origin recebe 100.000 requests por segundo, um aumento de 567%, até que o cache se reaqueça.
O tempo de reaquecimento depende do volume de conteúdo único e do tráfego. Para grandes plataformas, pode levar de 10 a 90 segundos. Nesse intervalo, o origin precisa sobreviver a uma carga para a qual provavelmente não foi dimensionado. A alternativa, dimensionar o origin para suportar tráfego sem CDN, é financeiramente inviável para a maioria das operações.
A lição empírica, aprendida da forma mais dolorosa possível em salas de guerra de Black Friday e incidentes de produção às 3h da manhã, é que uma CDN bem configurada não é um componente de infraestrutura passivo. Ela é um estado distribuído que você gerencia ativamente e gerenciar estado distribuído é, por definição, um dos problemas mais complexos da engenharia de software.
Para onde ir a partir daqui
A invalidação de cache em sistemas distribuídos não tem solução definitiva. Tem um conjunto de trade-offs que você escolhe conscientemente com base nos requisitos específicos da sua aplicação, tolerância a dados desatualizados, custo de infraestrutura, complexidade operacional e volume de eventos de mudança.
O que distingue times que lidam bem com esse problema dos que ficam perpetuamente em modo reativo é a consciência de que cada decisão de TTL e cada estratégia de purge tem um custo real, mensurável, que vai além da linha de cache hit ratio no dashboard.
Medir a janela de inconsistência do seu ambiente específico, mapear a volatilidade de cada componente que você serve e tratar invalidação como um sistema de primeira classe, com observabilidade, retry, backpressure e alerta, é o que separa uma arquitetura de CDN que serve bem em dias normais de uma que sobrevive ao dia em que você mais precisa dela.
E no dia em que você mais precisa dela, geralmente é porque algo mudou. E quando algo muda, invalidar é o que você vai precisar fazer. O custo dessa operação, como você já sabe agora, é muito maior do que o fornecedor de CDN quer que você pense.





