Fila SQS para trabalho pesado num Next.js com SST
Indexar um documento leva minutos, então joguei isso pra uma fila SQS com Lambda, no mesmo sst.config do site. Quase todo o trabalho foi acertar timeout, DLQ e arm64.
No portal em Next.js, publicar um documento dispara trabalho pesado: quebrar o texto em trechos, gerar embeddings, chamar um LLM pra enriquecer cada trecho e gravar tudo no índice de busca. Isso leva minutos e não cabe numa requisição, então joguei pra uma fila SQS com uma Lambda consumindo, tudo no mesmo sst.config.ts do site.
A fila
const dlq = new sst.aws.Queue("IngestDlq");
const queue = new sst.aws.Queue("IngestQueue", {
visibilityTimeout: "940 seconds",
dlq: { queue: dlq.arn, retry: 3 },
});
O visibility timeout tem que ser maior que o timeout da Lambda. A Lambda pode rodar até 15 minutos (900 s), e se a mensagem voltar a ficar visível antes disso, outra invocação pega o mesmo documento enquanto a primeira ainda está trabalhando. Com um valor menor, o próprio SQS recusa a ligação com a Lambda no deploy.
E sem DLQ, o erro fica rodando pra sempre. Um documento que sempre falha volta pra fila até acabar o prazo de retenção, e a cada volta ocupa o banco de novo. Com retry: 3, depois de três tentativas ele para num lugar onde dá pra olhar e reprocessar.
Um documento por vez
queue.subscribe(
{
handler: "lambdas/ingest.handler",
timeout: "15 minutes",
memory: "512 MB",
architecture: "arm64",
nodejs: { install: ["@libsql/linux-arm64-gnu"] },
},
{ batch: { size: 1, partialResponses: true } },
);
Lote de 1 parece desperdício, mas gravar um documento segura o escritor do banco (Turso) por dezenas de segundos. Com lote grande, um documento lento atrasa os outros, e um que falha leva os vizinhos junto pra DLQ. Sai mais invocação, o que ainda é barato perto de reindexar.
Falha por mensagem, não por lote
Com partialResponses: true, a Lambda devolve só as mensagens que falharam, e só elas voltam pra fila:
export async function handler(event: SQSEvent) {
const failures: { itemIdentifier: string }[] = [];
for (const record of event.Records) {
try {
await processJob(JSON.parse(record.body));
} catch (error) {
console.error("ingest failed", record.messageId, error);
failures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures: failures };
}
Antes disso, um documento malformado derrubava o lote inteiro, e os que já tinham dado certo eram processados de novo, pagando embedding e LLM outra vez. O mesmo código serve pra qualquer tamanho de lote.
Se for processar em paralelo dentro do lote, coloque um limite de concorrência em vez de um Promise.all sem teto. Mandar tudo de uma vez estoura o limite de requisições do gateway de LLM.
O binário que o bundler esquece
O cliente do libSQL usa um binário nativo, e o bundler do SST não empacota ele. Sem a linha nodejs.install, a Lambda morre com Cannot find module. O sufixo do pacote (linux-arm64-gnu) tem que bater com a arquitetura que você declarou.
O site precisa do mesmo cuidado. O SST cria duas funções internas pra revalidação do ISR (o seeder e o subscriber de eventos), e elas nascem em x86_64. Se o bundle foi gerado em arm64, elas têm que acompanhar:
new sst.aws.Nextjs("Site", {
link: [queue],
transform: {
server(args) {
args.architecture = "arm64";
},
revalidationSeeder(args) {
args.architecture = "arm64";
},
revalidationEventsSubscriber(args) {
args.architecture = "arm64";
},
},
});
A fila na hora, o cron por garantia
É o link: [queue] no site que deixa o Next.js publicar na fila assim que um documento é aprovado. A indexação começa na hora, sem esperar a próxima rodada do agendamento.
Mesmo assim tem um cron de hora em hora que compara o índice com a fonte e enfileira o que ficou pra trás. Se uma mensagem se perder, o documento entra na busca na rodada seguinte do cron.
De brinde, uma variável de ambiente
Nessa mesma configuração, o rastreamento do Langfuse sumiu sem dar erro. O código lia LANGFUSE_HOST, o .env definia LANGFUSE_BASE_URL, a variável chegava vazia e o SDK caía no servidor padrão da Europa com as chaves de uma conta dos EUA. Nenhum trace, nenhum erro. Hoje a Lambda recebe LANGFUSE_BASE_URL ?? LANGFUSE_HOST, e eu confiro o primeiro trace depois de todo deploy.