Cloudflare
KV
Cloudflare KV
Cache Invalidation
Consistência
Estratégia
Edge

Invalidação de cache no KV: o problema que ninguém resolve com elegância

Como lidar com invalidação de cache no Cloudflare KV: chaves versionadas, CDN purge coordenado, stale-while-revalidate e o que não funciona quando você precisa de consistência imediata.

Phil Karlton disse que existem apenas duas coisas difíceis em ciência da computação: invalidação de cache e nomear coisas. O KV torna a primeira ainda mais difícil, porque você não controla quando a invalidação chega em cada ponto de presença.

Deletar uma chave no KV não a remove do mundo imediatamente. O delete vai para o store central e propaga para os PoPs com a mesma dinâmica de consistência eventual das escritas: até 60 segundos para alcançar todos os pontos de presença. Durante essa janela, PoPs que ainda não receberam o delete continuam servindo o valor antigo para qualquer request que chegar. Você não sabe quais PoPs receberam e quais não. Não há como forçar propagação imediata.

Isso torna a invalidação de cache no KV um problema de design, não de operação. Você não resolve com um botão de "invalidar agora" — resolve escolhendo um modelo de chaves que minimiza o impacto do delay de propagação.

Chaves versionadas: a solução mais robusta

O padrão mais sólido para conteúdo que precisa ser invalidado é manter uma indireção explícita entre o nome lógico do dado e a chave física onde ele está armazenado.

Em vez de escrever o HTML renderizado de uma página em homepage-html e depois deletar quando o conteúdo muda, você escreve em homepage-html-v42. Uma segunda chave, homepage-html-version, contém apenas o string v42. O Worker lê a chave de versão primeiro, constrói o nome da chave de conteúdo, e busca o valor.

Quando o conteúdo muda, você escreve o novo HTML em homepage-html-v43 e atualiza homepage-html-version para v43. A chave antiga não precisa ser deletada imediatamente — ela simplesmente deixa de ser referenciada. O custo de storage dela continua existindo até você fazer cleanup, mas a inconsistência de invalidação deixa de ser um problema: qualquer PoP que recebeu a atualização de homepage-html-version já vai buscar a chave nova. O conteúdo antigo é servido apenas para PoPs que ainda não receberam a atualização da chave de versão, e isso acontece dentro da janela de 60 segundos independente do que você fizer.

O custo dessa abordagem é uma leitura extra por request — primeiro a chave de versão, depois o conteúdo. Para a maioria dos casos, esse custo é irrelevante em termos de latência: as duas leituras são paralelas se o Worker as fizer com Promise.all, e ambas chegam do cache do PoP em sub-milissegundo quando quentes.

Coordenar KV com purge de CDN

Para conteúdo servido pela CDN do Cloudflare (não diretamente pelo Worker), existe uma camada adicional de cache acima do KV. O CDN pode ter cacheado uma resposta gerada com um valor antigo de KV, e mesmo que o KV já tenha o valor novo em todos os PoPs, a resposta cacheada na CDN ainda vai ser servida até expirar.

A solução é coordenar a atualização do KV com um purge da CDN. A API de Cache Purge do Cloudflare permite invalidar URLs específicas ou tags de cache por API. Você escreve o valor novo no KV e, na mesma operação administrativa, chama a API de purge para as URLs afetadas. Do ponto de vista dos clientes que passam pela CDN, a resposta nova aparece imediatamente após o purge — independente de quantos PoPs ainda estão propagando o valor KV.

Esse padrão exige que você controle o processo de atualização de conteúdo e tenha acesso à API de purge. Para fluxos de CMS onde um editor publica conteúdo, é uma integração razoável: o webhook de publicação atualiza o KV e dispara o purge.

Stale-while-revalidate com waitUntil

O padrão stale-while-revalidate serve conteúdo potencialmente desatualizado enquanto aciona uma atualização em background. No contexto de Workers, ctx.waitUntil() permite executar trabalho assíncrono após o response ser enviado ao cliente.

A implementação típica: o Worker lê o valor atual do KV e serve imediatamente. Ao mesmo tempo, dispara via ctx.waitUntil() uma função que verifica se o valor precisa ser atualizado — consultando uma origem, por exemplo — e escreve o valor novo no KV se necessário. O cliente recebe a resposta sem esperar pela atualização. O próximo request já pode receber o valor novo, dependendo de quando a propagação completar.

O tradeoff é explícito: você troca latência zero para o cliente por uma janela em que conteúdo desatualizado pode ser servido. Para a maioria dos cenários de cache de conteúdo, esse tradeoff é aceitável. Para dados onde staleness tem consequência operacional — preços, disponibilidade de estoque, permissões de acesso — o padrão não é adequado.

TTL como substituto de invalidação explícita

Para casos onde invalidação precisa não é necessária, TTL é o mecanismo mais simples. O KV suporta expirationTtl (segundos a partir de agora) e expiration (Unix timestamp) definidos no momento da escrita.

Uma flag de feature com TTL de 60 segundos expira automaticamente. O próximo request após a expiração vai buscar no store central e retornar o valor mais recente — ou um valor vazio, que o Worker pode interpretar como "flag desativada". Você não precisa deletar explicitamente nem rastrear o estado.

Para conteúdo com frequência de atualização previsível, TTL alinhado ao ciclo de atualização elimina a necessidade de invalidação ativa. Um relatório gerado toda hora pode ter TTL de 3600 segundos. O valor mais antigo possível que qualquer usuário vai ver é aproximadamente uma hora, e isso é uma escolha de design, não uma falha de consistência.

O que não funciona: delete e re-escrita imediata

Um padrão que aparece frequentemente e não resolve o problema é deletar a chave antiga e escrever a nova na sequência. Isso não elimina a janela de inconsistência — as duas operações propagam de forma independente para os PoPs. Um PoP pode receber o delete mas ainda não ter recebido a nova escrita, e durante essa janela vai retornar não-encontrado para aquela chave. Dependendo de como o Worker trata esse caso, isso pode resultar em erro ou em fallback inesperado.

Deletar e re-escrever tem o dobro das operações de propagação com o triplo dos estados intermediários possíveis: antigo, não-encontrado, novo. Chaves versionadas ou TTL são sempre preferíveis.

Como a consistência eventual muda o design

O padrão mais saudável para trabalhar com KV é aceitar a consistência eventual como uma característica do sistema, não como uma limitação a ser contornada. Isso significa desenhar fluxos onde uma janela de até 60 segundos de inconsistência é tolerável, e usar ferramentas diferentes quando não é.

Para dados que exigem consistência imediata em todos os PoPs, KV não é a ferramenta correta. D1 com leituras direcionadas ao primary, ou Durable Objects para estado com acesso serializado, atendem casos onde o modelo de eventual consistency do KV não serve.

A invalidação elegante no KV é aquela que não precisa acontecer de forma urgente — porque o design do sistema foi feito para tolerar a janela de propagação. Qualquer tentativa de forçar consistência imediata vai trabalhar contra a arquitetura, não com ela.

Leia também

Invalidação de cache no KV: o problema que ninguém resolve com elegância | Matheus Breguêz