Feed XML que quebrava em catálogos grandes
Feeds de produtos com alguns milhares de itens saíam truncados, e aumentar a memória só adiava o problema. Diagnostiquei a causa e redesenhei a geração: em lotes, fora do request, com trava de concorrência e escrita atômica, para qualquer tamanho de catálogo.
- Papel
- Desenvolvedor responsável, do diagnóstico à entrega
- Contexto
- Plataforma de e-commerce multi-tenant
- Stack
- PHP, Symfony, Doctrine, Redis, Twig, XML
- Time
- Solo
- Período
- 2026
Case anonimizado: nomes de empresa, cliente e pessoas foram omitidos ou trocados.
01. Situação
Acima de alguns milhares de produtos, o feed saía cortado
As lojas da plataforma publicam feeds XML de produtos para serviços como comparadores de preço e plataformas de avaliação. Cada loja tem o seu template, guardado no banco. Numa loja com cerca de 7.000 produtos, o feed de uma categoria com pouco mais de mil itens funcionava, mas o de uma categoria com 3.331 saía truncado: tags sem fechar, nenhum arquivo de cache e estouro de memória no log.
Reproduzi o problema com categorias de tamanhos diferentes para achar o ponto em que ele aparecia. O contorno óbvio era dar mais memória ao PHP, e eu testei: de 512 MB para 2 GB, o feed de 3.331 passou, muito lento, e o feed com todos os produtos voltou a quebrar.
- produtos: feed gerado
- 1.055
- produtos: feed truncado
- 3.331
- produtos: quebra mesmo com 2 GB
- ~7.000
02. Investigação
O consumo crescia com o catálogo por três caminhos ao mesmo tempo
O teste com 2 GB me mostrou que o problema não era um limite mal configurado: o consumo crescia junto com o volume, então qualquer limite seria atingido por um catálogo maior. Fui atrás de onde a memória ia. Li o fluxo inteiro da geração, contei as consultas por feed e mapeei o padrão de uso de memória.
O controller buscava todos os produtos da categoria em uma consulta e mantinha todas as entidades do ORM na memória até o fim.
- Pedido do feed
- Busca todos os produtos de uma vez
- Template monta o XML inteiro em memória, com consultas por produto
- Resposta e cache gravados só no final
- memória estoura no meio: XML truncado e nenhum cache gravado
Esse desenho tinha um segundo defeito: como o cache só era gravado no final, um feed que quebrava nunca gerava cache, e cada novo pedido repetia o trabalho inteiro e quebrava de novo.
03. Solução
Gerar em lotes, fora do request, e publicar só o arquivo completo
Decidi não tratar o sintoma. Em vez de ajustar limites, redesenhei o fluxo de geração para que a memória usada deixasse de depender do tamanho do catálogo.
A busca passou a trazer só os ids. A renderização acontece em lotes de 200 produtos: cada lote é carregado, renderizado, escrito direto no arquivo e descartado, com a memória do ORM limpa antes do lote seguinte. Para isso funcionar com templates escritos pelas lojas, implementei a detecção do cabeçalho e do rodapé do template, que são escritos uma vez só, em volta dos lotes.
Feeds acima de mil produtos saem do request: são gerados por um comando em segundo plano, e o pedido HTTP só entrega o que já está pronto.
- Pedido do feed
- Cache válido: entrega o arquivo
- Feed grande: pega a trava e dispara a geração em segundo plano
- Lotes de 200 escritos em um arquivo temporário
- Arquivo completo substitui o cache
- trava ocupada: entrega o cache anterior ou uma resposta provisória
- produtos por lote
- 200
- produtos: geração em segundo plano
- 1.000+
- validade do cache
- 6 h
04. Concorrência
Uma geração por vez, e nenhum arquivo pela metade
Tirar a geração do request criou problemas novos, e eu tratei cada um antes de entregar.
Escolha
Trava distribuída no Redis
Vários pedidos do mesmo feed não podem disparar várias gerações. Usei uma trava por feed, com validade de 10 minutos, que funciona entre servidores e só pode ser liberada por quem a pegou.
Escolha
Escrita atômica do cache
O arquivo é escrito em um temporário e só troca de nome quando está completo. Quem pede o feed recebe a versão anterior ou a nova, nunca um XML pela metade.
Escolha
Resposta provisória na primeira geração
Enquanto o primeiro arquivo de um feed grande não existe, o pedido recebe uma resposta provisória em vez de ficar preso esperando.
Cuidado
Corrida e arquivos órfãos
Nos testes, encontrei e corrigi uma condição de corrida e arquivos temporários que ficavam órfãos.
05. Compatibilidade
Os templates das lojas continuam funcionando sem alteração
Cada loja escreve o próprio template, então eu não podia assumir um formato. Mantive a mudança dentro do fluxo de XML, sem tocar nos componentes de template compartilhados com o resto da plataforma, e cobri os casos em que a geração em lotes não se aplica.
Trade-off
Não reescrever as funções do template
Reduzir as consultas por produto exigiria mexer em componentes usados pela loja inteira. Resolvi o consumo de memória no fluxo do XML, com risco restrito a ele.
Cuidado
Nomes de feed que colidem
Dois nomes diferentes podiam resultar no mesmo arquivo de cache. Adicionei a validação no cadastro do feed, com testes.
06. Resultado
O feed sai completo para qualquer tamanho de catálogo
O feed grande deixou de depender da memória do request. Ele é gerado por partes, em segundo plano, com uma geração por vez, e servido do cache por seis horas. O feed que saía truncado passou a sair completo, e o tamanho do catálogo deixou de ser um limite.
Além do código, entreguei a análise técnica do módulo de XML documentada para o time, logs próprios da geração e um comando de geração de cache que pode ser reaproveitado.