Microserviços com Node.js: Comunicação, Resiliência e Observabilidade
Um serviço HTTP que chama outro serviço HTTP que chama outro serviço HTTP. Quando o terceiro demora 5 segundos para responder, o primeiro segura a conexão, o segundo acumula requests na fila, e o usuário vê um spinner eterno. Três serviços, um único ponto de falha cascateado.
Microserviços não resolvem problemas de complexidade: eles redistribuem a complexidade para a rede. E a rede falha. A questão não é SE um serviço vai ficar indisponível, mas QUANDO, e como o sistema se comporta nesse cenário.
Este post cobre três pilares que separam microserviços funcionais de microserviços frágeis: comunicação (síncrona e assíncrona), resiliência (circuit breaker, retry, timeout) e observabilidade (tracing distribuído com OpenTelemetry).
Comunicação síncrona vs assíncrona: quando usar cada uma
A escolha entre HTTP direto e mensageria define o acoplamento temporal entre serviços. Se o serviço A precisa da resposta do serviço B para continuar, a comunicação é síncrona. Se o serviço A dispara um evento e segue em frente, é assíncrona.
| Critério | HTTP síncrono | Mensageria assíncrona |
|---|---|---|
| Latência percebida | Soma das latências de toda a cadeia | Resposta imediata ao cliente, processamento em background |
| Acoplamento temporal | Alto: se B cai, A falha | Baixo: se B cai, a mensagem espera na fila |
| Complexidade de debug | Menor (request/response linear) | Maior (mensagens em filas, ordem não garantida, dead letter queues) |
| Consistência de dados | Mais fácil de garantir por transação | Eventual consistency, exige idempotência |
| Caso de uso típico | Consulta de dados em tempo real, validação | Processamento de pedidos, envio de e-mails, geração de relatórios |
Se o usuário precisa ver o resultado na mesma request (consulta de saldo, validação de CPF), use HTTP. Se o resultado pode chegar depois (confirmação de pagamento, geração de PDF), use mensageria. Já cobrimos o lado assíncrono em detalhes no post sobre mensageria com BullMQ e Redis.
Chamadas HTTP entre serviços com timeout e retry
Uma chamada HTTP entre serviços sem timeout é uma bomba-relógio. O default do Node.js (runtime que usa libuv para I/O) para http.request não tem timeout de resposta: ele espera indefinidamente. Isso significa que um serviço lento trava quem o chama.
// src/infra/http-client.ts
import { setTimeout } from "node:timers/promises";
interface RetryConfig {
maxRetries: number;
baseDelayMs: number;
timeoutMs: number;
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelayMs: 200,
timeoutMs: 3000,
};
export async function fetchWithRetry(
url: string,
options: RequestInit = {},
config: RetryConfig = DEFAULT_CONFIG
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
const controller = new AbortController();
// Timeout via AbortController: se o serviço não responder
// em timeoutMs, a request é cancelada no nível de socket
const timeoutId = globalThis.setTimeout(
() => controller.abort(),
config.timeoutMs
);
const response = await fetch(url, {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
// Retry apenas em erros de servidor (5xx), não em 4xx
// porque 4xx indica erro do chamador, não indisponibilidade
if (response.status >= 500 && attempt < config.maxRetries) {
lastError = new Error(`HTTP ${response.status} from ${url}`);
const delay = config.baseDelayMs * Math.pow(2, attempt);
await setTimeout(delay);
continue;
}
return response;
} catch (error) {
lastError = error as Error;
if (attempt < config.maxRetries) {
// Exponential backoff: 200ms, 400ms, 800ms
// Evita thundering herd quando o serviço volta
const delay = config.baseDelayMs * Math.
---
Leia o artigo completo em [https://www.vivodecodigo.com.br/backend/microservicos-nodejs-comunicacao-resiliencia-observabilidade](https://www.vivodecodigo.com.br/backend/microservicos-nodejs-comunicacao-resiliencia-observabilidade)