Uma loja de persianas sob medida cria uma variante de produto a cada configuração que o cliente monta. Largura, altura, tecido, cor, acionamento: cada combinação vira um item de verdade no catálogo, senão não entra no carrinho. Isso significa que o catálogo enche de lixo por construção, e que existe um Worker agendado cuja única função é apagar as variantes que ninguém comprou.
A suspeita registrada há duas semanas era direta: os IDs das variantes estavam espalhados numa faixa larga demais para uma janela de 24 horas, então o cron provavelmente não estava rodando.
A suspeita acertou o sintoma e errou a causa inteira. O cron rodava. Rodava havia meses, sem uma falha de execução. E mesmo assim havia 100 variantes vencidas, a mais antiga criada 101 dias antes.
O padrão que entregou o caso
Antes de mexer em qualquer linha, rodei um script somente leitura contra a loja para contar o que existia de fato. O resultado não parecia aleatório, e é isso que interessa:
- 109 variantes do configurador vivas, das quais 100 já vencidas.
- Todas as 100 vencidas estavam em produtos da posição 79 em diante da lista.
- O único produto do configurador dentro das primeiras 47 posições tinha zero vencidas.
Onde o cron alcançava, ele funcionava perfeitamente. O problema não era execução, era alcance. E “alcance” não é uma categoria de bug que ocorre a você quando a hipótese de partida é “não está rodando”.
Três causas somadas
Nenhuma das três sozinha explicava o acúmulo.
A janela nunca foi 24 horas. A constante no código dizia 72, com um comentário explicando o motivo: dar três dias para o cliente voltar ao carrinho abandonado. O README, a documentação e minhas próprias notas diziam 24. A suspeita original tinha sido medida contra a janela errada, e com 72 horas o espalhamento esperado de IDs é três vezes maior. Ou seja, o dado que levantou a suspeita não era nem evidência de problema.
O filtro por nome pegava a loja inteira. A lista de produtos do configurador era identificada por
nome, procurando por termos como “blackout” e “tela solar”. Só que a loja vende persianas: esses
termos casavam com 111 dos 113 produtos. Listar as variantes de 111 produtos custa 111 subrequests, e
o plano grátis do Cloudflare Workers dá um teto de 50. O loop estourava o orçamento por volta do
produto 47 e dava break. Sempre nos mesmos 47 primeiros. Os produtos que realmente acumulavam lixo
viviam nas posições 79 a 111.
O agendamento era semanal. O painel mostrava 0 3 * * 0, domingo às 3h. O comentário no código,
o README e a documentação diziam 0 3 * * *, diário. Nunca foi diário.
O corolário que explica por que ninguém tinha resolvido na mão
Existia uma rota manual de limpeza, para forçar a faxina sem esperar o agendamento. Ela chamava exatamente a mesma função, que começa sempre do início da lista.
Rodar a limpeza manual, quantas vezes fosse, gastava o orçamento inteiro percorrendo produtos que já estavam limpos e parava antes de chegar nos sujos. A válvula de escape tinha o mesmo defeito do sistema que ela deveria socorrer, o que a tornava perfeitamente inútil de um jeito silencioso.
A correção não foi aumentar o orçamento
A saída óbvia seria paginar, guardar um cursor entre execuções e ir rodando por partes. Cheguei a considerar. Foi a leitura da documentação da API que tornou isso desnecessário: o endpoint de produtos já devolve as variantes embutidas em cada produto, com data de criação e tudo.
Os 113 produtos vêm em uma subrequest. Não 111.
Com isso a varredura passou a montar uma fila global de vencidas, ordenada da mais antiga para a mais nova, e a gastar as ~47 chamadas restantes só em exclusões. Como cada execução enxerga a loja inteira, qualquer acúmulo se drena sozinho execução após execução, sem precisar guardar estado em lugar nenhum.
O rodízio entre execuções que eu tinha cogitado só existia como ideia porque listar variantes custava caro. Quando o custo caiu de 111 para 1, o problema que o rodízio resolvia deixou de existir. Vale como regra geral: boa parte da complexidade que a gente projeta existe para contornar uma restrição que nunca foi verificada.
O achado colateral que quase virou incidente
Duas coisas apareceram por acidente no meio do diagnóstico, e as duas eram mais perigosas que o bug original.
A primeira: a rota manual de limpeza chamava a função com o parâmetro que ignora a janela de tempo. O nome e o uso pretendido eram “adiantar o cron”, mas o comportamento real era apagar todas as variantes, inclusive as de carrinhos abertos naquele instante. Quem clicasse achando que estava só antecipando a faxina destruiria compras em andamento. O padrão passou a respeitar a janela, igual ao agendamento, e o modo destrutivo passou a exigir um parâmetro explícito na URL.
A segunda: depois de drenar as 100 variantes, um dos produtos ficou com uma única variante, aquela que eu vinha tratando havia semanas como lixo cosmético. Ela é o que mantém viva a propriedade de variação do produto. Apagá-la deixaria o produto sem variante nenhuma, e a plataforma remove o atributo nesse caso, o que quebraria a criação de variantes futuras. O configurador daquela página simplesmente pararia de funcionar.
Uma decisão anterior de não mexer nela tinha sido tomada por outro motivo, e por sorte. Ela não era cosmética, era estrutural.
O que ficou
A drenagem apagou 100 variantes, com 0 erros e nenhum produto zerado. Sobraram 9, todas dentro da janela de 72 horas, a mais antiga com 47 horas: carrinhos legítimos, que é exatamente o que deveria sobrar.
Antes de subir, validei a lógica nova simulada contra a loja real, sem apagar nada: 1 chamada de varredura contra as 112 de antes, 47 sobrando para exclusões, e no pior caso simulado a fila saiu ordenada corretamente, com a variante estrutural fora dela.
O que eu tiro daqui, e que não é sobre cron:
Um agendamento que executa sem erro não é um agendamento que funciona. O log dizia “sucesso” todas as vezes, porque terminar o orçamento e sair é um caminho de sucesso. Só medir o efeito no mundo, e não o status da execução, revelou o problema.
Três documentos diziam a mesma coisa errada. README, documentação e minhas notas concordavam entre si sobre a janela e sobre a frequência. Concordância entre documentos não é verificação: eles tinham sido copiados uns dos outros. O código e o painel discordavam dos três, e estavam certos.
A hipótese inicial custou duas semanas. “O cron não está rodando” era plausível, era compatível com o sintoma, e mandou olhar para o lugar errado. O que destravou foi parar de testar a hipótese e ir contar o que existia na loja.