Por que background jobs importam em Rust
Nem todo trabalho deveria acontecer dentro de uma requisição HTTP. Enviar e-mail, processar imagem, sincronizar CRM, recalcular relatório, consumir webhook, gerar arquivo, enriquecer evento, consultar uma API lenta ou reprocessar falha são tarefas que normalmente pedem background jobs. Se tudo isso fica preso ao tempo de resposta do usuário, a aplicação vira frágil: uma dependência lenta derruba checkout, uma integração instável aumenta latência e um pico de tráfego cria efeito dominó.
Rust combina bem com esse tipo de problema porque entrega binários leves, concorrência explícita e tratamento de erro sem exceções escondidas. Mas a linguagem não transforma automaticamente um worker em sistema confiável. Um job pode duplicar efeito, crescer fila sem limite, perder mensagem no shutdown, ficar invisível em logs ou repetir uma chamada externa até derrubar o parceiro. O diferencial profissional está em desenhar a operação, não apenas em compilar um loop assíncrono.
Para devs brasileiros mirando vagas Rust de backend, plataforma, fintech, dados ou infraestrutura, background jobs são um tema forte. Muitas descrições de vaga falam em sistemas distribuídos, filas, processamento assíncrono, observabilidade, cloud, Kubernetes e confiabilidade. Mesmo quando a vaga não usa a expressão “background job”, ela espera que você saiba tirar trabalho pesado do caminho síncrono e operá-lo com segurança.
Comece pelo contrato do job
Um job não deveria ser apenas um JSON solto jogado em uma fila. Ele precisa de contrato. Em Rust, modele o payload com Serde, valide campos obrigatórios e inclua metadados operacionais: job_id, tipo, versão do schema, data de criação, tentativa atual, origem e uma chave de idempotência quando houver efeito externo.
Um contrato simples para envio de e-mail poderia carregar job_id, recipient_id, template, locale, created_at e idempotency_key. O worker não precisa receber tudo que estava na requisição original. Ele deve receber o mínimo suficiente para buscar dados atuais, aplicar regra de negócio e registrar resultado. Isso reduz vazamento de dados sensíveis e evita que payloads antigos carreguem estado obsoleto.
Versionamento importa. Jobs podem ficar na fila enquanto você faz deploy. Se a versão nova do worker não entende mensagens antigas, você criou uma bomba-relógio. Uma regra prática é tornar mudanças compatíveis: adicionar campos opcionais, aceitar versão anterior por um período e só remover suporte depois que a fila antiga drenou.
Escolha a fila pelo comportamento, não pela moda
Para um produto pequeno, uma tabela jobs no PostgreSQL pode ser suficiente: transação com o estado de negócio, consulta por status, locked_at, tentativas e índice por prioridade. É simples de auditar e funciona bem quando o volume é moderado. Com SQLx, você mantém queries explícitas e tipadas.
Redis pode servir para filas leves, deduplicação e jobs curtos. O artigo sobre Redis com Rust aprofunda cache, filas leves e locks. RabbitMQ faz sentido para filas de trabalho, roteamento e dead letters. Kafka brilha em eventos com replay e particionamento por chave. NATS é forte quando a prioridade é simplicidade operacional e baixa latência. O guia de Rust para mensageria compara esses caminhos.
A pergunta central é: o que acontece quando o worker cai no meio? A mensagem volta? O offset só avança depois do sucesso? Existe dead letter? Você consegue reprocessar uma classe de erro? Consegue ver o tamanho da fila? Consegue pausar consumo sem perder dados? A escolha da fila precisa responder isso antes de discutir benchmark.
Backpressure: limite é recurso de produto
Um worker sem backpressure é uma fila de incidentes. Se chegam 10.000 jobs por minuto e você processa 1.000, a diferença precisa aparecer em algum lugar: backlog, latência, uso de memória, banco, custo de API externa ou erro para o usuário. Ignorar o limite não remove o problema; só muda onde ele explode.
Em Tokio, comece com primitivas simples. mpsc::channel(capacity) cria um canal limitado. Quando a fila interna enche, o produtor espera ou recebe erro dependendo da API usada. Semaphore limita concorrência para chamadas externas. timeout impede job preso para sempre. JoinSet ajuda a coordenar tarefas dinâmicas sem perder handles.
O ponto não é usar todas as primitivas; é escrever a política. Para jobs críticos, talvez você aceite backlog e escale workers. Para eventos analíticos, talvez você descarte amostras antigas quando a fila cresce. Para webhook financeiro, talvez você pare de consumir, alerte e preserve ordem. Para e-mail promocional, talvez atrase. Backpressure é decisão de negócio expressa em código.
Idempotência antes de retry
Retry é útil, mas perigoso. Se uma API externa falha por timeout, você não sabe se ela não recebeu a chamada ou se processou e apenas não respondeu. Se você tenta novamente sem idempotência, pode enviar dois e-mails, criar dois pagamentos, duplicar uma nota fiscal ou registrar dois créditos.
Antes de adicionar retry, defina uma chave estável. Para envio de e-mail transacional, pode ser user_id + template + business_event_id. Para cobrança, use a chave idempotente fornecida pelo provedor de pagamento. Para atualização de CRM, registre o último evento aplicado. Para geração de relatório, grave artefato por versão de entrada.
No banco, uma tabela de job_executions com job_id, idempotency_key, status, tentativas e resultado ajuda a tornar o comportamento auditável. Em alguns casos, uma constraint unique já evita duplicidade. Em outros, você precisa de transação: marcar início, executar efeito, gravar conclusão e só então confirmar a mensagem da fila.
Erros permanentes, transitórios e dead letters
Nem todo erro merece retry. Payload inválido, usuário inexistente, template removido ou regra de negócio rejeitada são erros permanentes. Repetir dez vezes só polui logs. Timeout de API, indisponibilidade temporária do banco, rate limit ou falha de rede podem ser transitórios. Esses merecem retry com backoff e limite.
Classifique erros no tipo, não apenas na mensagem. Um enum como JobError::Permanent, JobError::Retryable e JobError::Poison já muda a operação. O worker passa a decidir: concluir como descartado com motivo, reagendar com atraso, enviar para dead letter ou abrir alerta.
Dead letter não é lixeira. É uma fila de investigação. Ela precisa preservar payload, erro, tentativas, timestamps e versão do worker. Se ninguém olha dead letters, você só criou um cemitério silencioso. Conecte dead letters a métricas e a um procedimento de reprocessamento.
Observabilidade do worker
Um background job invisível é pior do que uma requisição lenta, porque o usuário talvez nem veja erro imediato. Use Tracing para registrar job_id, tipo, tentativa, fila, duração, resultado e classe de erro. Evite logar payload inteiro; ele pode conter dados pessoais, tokens ou informações de cliente.
Métricas mínimas incluem jobs recebidos, concluídos, falhos, retries, dead letters, duração por tipo, backlog, idade do job mais antigo e concorrência ativa. Para SLO, a idade do job mais antigo costuma ser mais útil que média de processamento. Uma fila pode processar rápido os jobs novos enquanto um conjunto antigo está preso por erro permanente.
Se o job participa de uma requisição maior, propague contexto distribuído. OpenTelemetry pode carregar traceparent em headers HTTP, gRPC e metadados de fila. O artigo sobre OpenTelemetry em Rust mostra como pensar essa correlação entre API, banco, filas e workers.
Shutdown gracioso e deploy
Workers também precisam parar direito. Durante deploy, Kubernetes, systemd ou Docker enviam sinal de término. Se o processo encerra imediatamente, perde trabalho em andamento. Se ignora o sinal, o orquestrador mata depois do prazo. O caminho saudável é parar de buscar novos jobs, aguardar jobs ativos até um timeout e devolver ou reagendar o que não terminou.
Com Tokio, escute SIGTERM ou use um token de cancelamento compartilhado. Cada loop de consumo deve verificar cancelamento entre jobs. Para jobs longos, divida em etapas com checkpoints. Não segure uma mensagem invisível por minutos sem renovar lease ou heartbeat quando a fila exigir isso.
Deploy também precisa considerar compatibilidade. Se você muda schema de payload, suba primeiro worker que entende versão antiga e nova. Se muda formato de resultado, garanta que consumidores downstream aceitam ambos. Se muda concorrência, monitore fila e dependências externas. Release de worker é release de sistema distribuído.
Projeto de portfólio para mostrar maturidade
Um bom projeto de portfólio Rust é um processador de webhooks idempotente. Publique uma API Axum que recebe eventos, valida assinatura, grava no PostgreSQL e enfileira processamento. Um worker Tokio consome jobs, simula chamada externa, aplica retry com backoff, registra resultado e envia erro permanente para dead letter. Adicione Docker Compose, migrations, logs estruturados, métricas simples e testes de idempotência.
Esse projeto conversa melhor com o mercado do que mais um CRUD. Ele mostra HTTP, banco, filas, concorrência, observabilidade, segurança operacional e deploy. Para empresas que usam Rust, sinaliza que você entende produção. Para comparar com outro ecossistema forte em backends e workers, veja também materiais do Go Brasil.
Checklist rápido
Antes de colocar um background job Rust em produção, pergunte:
- O payload tem
job_id, versão e chave de idempotência? - A fila define claramente ack, retry, atraso e dead letter?
- A concorrência é limitada por tipo de job e dependência externa?
- O worker diferencia erro permanente de transitório?
- Retry tem backoff, limite e métrica?
- Shutdown para de consumir antes de encerrar jobs ativos?
- Logs e métricas permitem investigar um
job_idespecífico? - O deploy é compatível com mensagens antigas ainda na fila?
- Existe procedimento para reprocessar dead letters com segurança?
- O usuário não fica esperando trabalho que deveria ser assíncrono?
Rust dá uma base excelente para workers confiáveis, mas confiabilidade vem do desenho completo: contrato, fila, idempotência, backpressure, observabilidade e operação. Quando isso aparece no código e no README, o projeto deixa de ser apenas “feito em Rust” e passa a parecer software pronto para produção.