Por que resiliência virou assunto de carreira Rust
Rust costuma entrar em conversas de backend por performance, segurança de memória e previsibilidade. Mas, em produção, a pergunta mais importante raramente é “quantas requisições por segundo este framework aguenta?”. A pergunta prática é: o que acontece quando algo dá errado? O banco fica lento, a API parceira oscila, o cliente dispara tráfego demais, uma fila acumula mensagens, o deploy reinicia pods no meio de uma chamada, o DNS atrasa, o Redis cai por alguns segundos ou um endpoint começa a receber payloads maiores do que o esperado.
Um serviço Rust resiliente não é aquele que nunca falha. É aquele que falha de forma limitada, observável e recuperável. Ele tem timeout em toda dependência externa, limite de concorrência para não se afogar, retry apenas quando é seguro, proteção contra abuso, logs estruturados, métricas úteis e shutdown gracioso. A linguagem ajuda porque força tratamento explícito de erros e reduz classes inteiras de bugs de memória, mas ela não decide sozinha política de retry, backpressure ou degradação.
Para devs brasileiros mirando vagas Rust de backend, plataforma, infraestrutura, fintech, dados ou developer tools, esse é um diferencial forte. Muitas descrições de vaga não dizem “resiliência” no título, mas pedem experiência com APIs em produção, sistemas distribuídos, observabilidade, cloud, Kubernetes, mensageria, incidentes e SLOs. Saber explicar como um serviço Axum se comporta sob falha vale mais do que apenas mostrar um CRUD funcionando localmente.
O papel de Tower no stack Rust moderno
Tower é a abstração que conecta boa parte do stack de rede do Rust. Axum, Hyper, Tonic e várias bibliotecas de cliente usam o modelo de Service e Layer: um serviço recebe uma requisição, produz uma resposta assíncrona e pode ser envolvido por camadas de middleware. Isso parece abstrato no começo, mas resolve um problema bem concreto: aplicar comportamento operacional de forma composável.
Em uma API HTTP, você pode empilhar camadas para request ID, tracing, timeout, compressão, CORS, autenticação, limite de concorrência e fallback. Em um serviço gRPC, o mesmo raciocínio aparece em interceptors, deadlines, limites de payload e observabilidade. O benefício não é só reuso de código. É criar uma linguagem comum entre projetos: toda entrada passa por uma sequência explícita de decisões antes de chegar no handler de negócio.
Esse modelo é especialmente útil porque resiliência não deveria ficar espalhada em cada handler. Se todo endpoint precisa lembrar manualmente de medir latência, checar limite, aplicar timeout e registrar erro, o sistema fica inconsistente. Camadas Tower permitem transformar políticas operacionais em infraestrutura do serviço. O handler continua focado na regra de negócio.
Timeouts: a primeira defesa contra cascata
Timeout é o mecanismo mais simples e mais esquecido. Sem timeout, uma chamada lenta ocupa conexão, memória, task e talvez uma transação no banco até algum limite externo encerrar. Em tráfego baixo, isso parece inofensivo. Em pico, vira cascata: a dependência lenta faz handlers acumularem, o runtime fica cheio de trabalho pendente, pools se esgotam e endpoints não relacionados começam a falhar.
Em Rust async, você tem ferramentas em vários níveis. tokio::time::timeout protege uma operação específica. Camadas de tower-http podem limitar tempo de resposta em rotas. Clientes HTTP como Reqwest permitem timeout por requisição. Bancos e pools costumam ter timeouts próprios. O ponto é definir orçamento de tempo de ponta a ponta, não apenas um número aleatório em uma chamada.
Uma regra prática: o timeout de uma chamada interna precisa ser menor que o timeout recebido pelo cliente. Se o load balancer fecha em 30 segundos, não faz sentido o serviço esperar 45 segundos por uma dependência. Se um endpoint chama três serviços internos, cada um precisa de orçamento menor e erro claro quando estourar. Também registre qual timeout disparou. “Erro interno” não ajuda em incidente; payment_provider_timeout com duração, rota e request ID ajuda.
Retry: quando repetir é seguro
Retry é tentador porque parece resolver instabilidade. Mas retry sem critério pode transformar uma falha pequena em avalanche. Se uma API parceira está lenta e todos os clientes tentam três vezes imediatamente, você triplica carga exatamente quando o sistema está degradado. Se a operação não é idempotente, retry pode duplicar pagamento, pedido, e-mail ou atualização de estado.
Antes de adicionar retry, classifique a operação. Leitura cacheável, consulta de status, chamada idempotente com chave estável e erro de rede transitório podem aceitar retry com backoff. Mutações financeiras, criação de recurso sem idempotência e comandos com efeitos externos precisam de cuidado. Em muitos casos, é melhor retornar erro claro e deixar uma fila idempotente ou reconciliação posterior lidar com a recuperação.
O padrão saudável combina três peças: número máximo pequeno de tentativas, backoff com jitter e métrica por motivo. Também é importante diferenciar erro permanente de erro transitório. 400 Bad Request, payload inválido e permissão negada não melhoram com retry. Timeout, conexão resetada e indisponibilidade temporária talvez melhorem. Essa distinção deve aparecer no tipo de erro ou na camada que traduz erro para resposta.
Limite de concorrência e load shedding
Um serviço não precisa aceitar tudo que chega. Em produção, aceitar requisições demais pode ser pior do que rejeitar cedo. Limite de concorrência protege CPU, memória, pool de banco e dependências downstream. Quando o limite é atingido, o serviço pode responder 503 Service Unavailable, 429 Too Many Requests ou outra resposta coerente em vez de enfileirar trabalho até morrer.
Esse é o raciocínio por trás de load shedding: descartar carga quando continuar aceitando pioraria a saúde do sistema. Para quem vem de aplicações tradicionais, isso soa agressivo. Na prática, é maturidade operacional. Um serviço que rejeita parte do tráfego com erro claro e se recupera em segundos costuma ser melhor do que um serviço que aceita tudo, aumenta latência por minutos e depois precisa ser reiniciado.
Em Axum, o desenho pode separar rotas por criticidade. Health check e métricas não deveriam disputar o mesmo orçamento de uma rota pesada. Endpoints públicos podem ter rate limit por IP ou token. Rotas internas podem ter limite de concorrência diferente. Operações administrativas caras podem exigir fila, não execução síncrona. A política deve refletir negócio, não apenas capacidade técnica.
Observabilidade: resiliência precisa aparecer nos dados
Não existe resiliência sem visibilidade. Se um timeout dispara mas ninguém sabe qual dependência falhou, você só transformou uma falha lenta em uma falha rápida. Tracing é a base natural no ecossistema Rust porque funciona bem com async, spans e campos estruturados. Para uma API, cada request deveria carregar request_id, rota, método, status, duração e campos de negócio seguros.
Também registre eventos operacionais: retry executado, retry abandonado, limite de concorrência atingido, load shedding aplicado, timeout por dependência, erro de serialização, queda de pool, shutdown iniciado e shutdown concluído. Esses eventos não precisam ser barulhentos em nível error; muitos são warn ou métricas. O importante é que o time consiga responder rapidamente: qual camada falhou, quantos usuários foram afetados, desde quando, e se a recuperação automática funcionou.
Métricas complementam logs. Conte requisições por rota e status, latência por percentil, número de timeouts, retries, rejeições por rate limit, tamanho de fila, uso de pool e erros por dependência. Para SLO, média quase nunca basta. P95 e P99 contam a história dos usuários que sofreram. Um serviço com média boa e cauda péssima ainda pode ser ruim para produto.
Graceful shutdown e deploy sem susto
Serviços resilientes também sabem parar. Em deploy, escala horizontal, node drain ou reinício de container, o processo precisa parar de aceitar novas requisições, terminar trabalho em andamento quando for seguro e encerrar antes do prazo do orquestrador. Se o serviço fecha imediatamente, clientes recebem erro. Se demora demais, o orquestrador mata o processo e você perde a chance de limpar recursos.
Com Tokio, é comum escutar sinais como SIGTERM, avisar o servidor HTTP, cancelar tasks controladas e aguardar conclusão com timeout. Para workers, a regra muda: pare de buscar novos jobs, finalize ou reenvie o job atual e grave estado suficiente para retry seguro. Para streams gRPC ou WebSocket, informe encerramento quando possível e evite deixar conexão pendurada.
Essa disciplina conversa com deploy. O artigo de deploy Rust em VPS cobre Docker, systemd e Nginx; a camada de resiliência complementa esse caminho. Não adianta ter Dockerfile pequeno se a aplicação não respeita sinal de parada, não tem health check útil e não diferencia pronto para receber tráfego de apenas processo vivo.
Projeto de portfólio: gateway resiliente em Axum
Um projeto forte para portfólio é um resilient-gateway pequeno. Ele expõe uma API Axum, chama uma dependência simulada, aplica timeout, limite de concorrência, rate limit, request ID, logs estruturados e métricas. Inclua endpoints para sucesso, lentidão, erro transitório e erro permanente. Mostre no README como cada falha aparece no log e qual resposta o cliente recebe.
Não precisa fingir escala de big tech. O valor está em demonstrar julgamento. Explique por que uma rota tem retry e outra não. Mostre como uma chave idempotente evitaria duplicidade. Adicione testes para timeout, limite excedido, erro de dependência e shutdown. Se quiser ir além, inclua uma versão gRPC com Tonic para mostrar que o mesmo raciocínio de Tower atravessa HTTP e comunicação interna tipada.
Para empresas que usam Rust, esse projeto comunica maturidade de produção. Ele conecta Rust, backend, observabilidade, arquitetura e operação. Se você também conhece Go, vale comparar com materiais do Golang Brasil, porque muitas equipes brasileiras avaliam Rust e Go para serviços de plataforma parecidos. A diferença não está apenas na sintaxe; está em como cada ecossistema expressa limites, contexto, middleware e tratamento de erro.
Checklist para revisar um serviço Rust
Antes de chamar uma API Rust de “pronta para produção”, revise:
- Toda chamada externa tem timeout explícito?
- Retry só existe em operações idempotentes ou claramente seguras?
- Há limite de concorrência por serviço, rota ou dependência crítica?
- Rate limit protege endpoints públicos ou caros?
- Erros de dependência viram respostas previsíveis, não panics?
- Logs carregam request ID, rota, status, duração e causa?
- Métricas mostram latência P95/P99, timeouts, retries e rejeições?
- Health check diferencia processo vivo de serviço pronto?
- Shutdown gracioso foi testado em deploy ou interrupção local?
- O README explica comportamento sob falha?
Esse checklist parece operacional, mas é exatamente onde Rust brilha quando usado com maturidade. O compilador reduz surpresas dentro do processo; Tower, Axum, Tokio e Tracing ajudam a reduzir surpresas ao redor dele. Para carreira, aprender essa camada transforma Rust de linguagem interessante em ferramenta séria para sistemas que precisam continuar funcionando quando o mundo real fica imperfeito.