Blog

Relógio dessincronizado entre serviços: quando a ordem dos eventos deixa de existir

O suporte abriu o chamado com uma frase que parecia impossível: o cliente respondeu à mensagem trinta e quatro milissegundos antes de ela ter sido enviada. Os dois carimbos de tempo estavam corretos do ponto de vista de cada serviço, e nenhum dos dois relógios estava quebrado. Este artigo mostra por que o carimbo de tempo de parede não é um mecanismo de ordenação, qual é a diferença prática entre desvio e salto de relógio e por que o segundo é o que realmente destrói dados, como um contador lógico por entidade resolve a ordenação sem depender de sincronia, por que a estratégia de última escrita vence apaga alterações silenciosamente sob dessincronia, qual carimbo usar quando o dado precisa ser ordenado e qual usar quando ele precisa ser auditado, e quais três alertas detectam a dessincronia antes de ela virar inconsistência no banco.

2026-09-02 / Arquitetura / 17 min

01

Dois carimbos corretos que descrevem uma ordem impossível

O incidente costuma aparecer como um dado absurdo, não como um erro. Uma resposta anterior à pergunta, um pedido cancelado antes de ser criado, uma sessão que durou menos zero vírgula dois segundos. Ninguém suspeita do relógio primeiro, porque o relógio é a peça de infraestrutura em que todo mundo confia sem pensar. A investigação começa procurando erro de fuso horário, depois erro de serialização, depois condição de corrida no código, e só no fim alguém compara a hora de duas máquinas e descobre que elas discordam em oitenta e sete milissegundos.

A causa raiz não é o desvio em si, é a suposição embutida no código. Quando o serviço A grava createdAt com o relógio dele e o serviço B grava updatedAt com o relógio dele, e alguma consulta depois ordena os dois campos juntos, o sistema está afirmando que existe uma linha do tempo única compartilhada por máquinas independentes. Essa linha do tempo não existe. O que existe é um conjunto de relógios que aproximam a mesma referência com erro variável, e o erro é pequeno o suficiente para passar despercebido em desenvolvimento e grande o suficiente para inverter a ordem de dois eventos separados por poucos milissegundos em produção.

LINHA DO TEMPO REAL (referencia absoluta, inobservavel)

  t=0ms          t=12ms
   |              |
   v              v
  envio          resposta

O QUE CADA SERVICO GRAVOU

  servico-mensageria (relogio +45ms)   envio    -> 10:00:00.045
  servico-inbox      (relogio -42ms)   resposta -> 10:00:00.011  (!)

CONSULTA QUE ORDENA POR carimbo

  10:00:00.011  resposta
  10:00:00.045  envio          <- ordem invertida, dados corretos

  desvio total entre os dois relogios: 87ms
  intervalo real entre os dois eventos: 12ms
  qualquer par de eventos separado por menos de 87ms pode inverter

A regra que sai desse desenho é o único parâmetro que importa: dois eventos gravados por máquinas diferentes só têm ordem confiável se o intervalo entre eles for maior que o desvio máximo entre os relógios envolvidos. Num ambiente com sincronização por NTP bem configurada em rede local, esse desvio fica na casa de poucos milissegundos. Em contêineres com relógio virtualizado, em máquinas virtuais que sofreram migração ao vivo ou em regiões diferentes com caminhos de rede assimétricos, ele passa facilmente de cem milissegundos. Toda decisão de ordenação baseada em carimbo de parede está apostando que os eventos nunca vão acontecer mais perto do que esse número, e essa aposta é perdida justamente sob carga, quando os eventos ficam mais próximos.

02

Desvio é ruído, salto é corrupção

Os dois problemas de relógio são tratados como se fossem o mesmo e produzem falhas de natureza diferente. O desvio é a diferença constante e pequena entre duas máquinas: ele embaralha a ordem de eventos próximos, o que é ruim, mas mantém a propriedade de que o tempo sempre avança. O salto é o momento em que o daemon de sincronização corrige o relógio de uma vez, movendo-o para trás. Nesse instante, uma única máquina passa a produzir carimbos que já foram usados, e a ordem deixa de existir até dentro do próprio processo.

O salto para trás é o que causa perda de dados de verdade, e ele é mais comum do que parece. Acontece quando uma máquina virtual é retomada de um estado suspenso, quando um contêiner sobe com o relógio herdado de um host desatualizado, quando o NTP é configurado para corrigir de uma vez em vez de acelerar ou desacelerar gradualmente, e quando alguém reinicia um servidor cujo relógio de hardware está errado. O sintoma no código é uma duração negativa, um cache que expira antes de ser gravado, um bloqueio distribuído cujo prazo já passou no momento em que foi adquirido.

// time/clock.js
// Separa o relogio de parede (data absoluta, pode saltar) do relogio
// monotonico (so avanca, nao tem significado absoluto). Misturar os dois
// e a origem da maioria dos bugs de tempo em producao.

// Errado: mede duracao com relogio de parede. Se o NTP corrigir o relogio
// para tras no meio da operacao, a duracao vem negativa e qualquer
// comparacao com limiar passa a decidir o oposto do pretendido.
export const medirErrado = async (fn) => {
  const inicio = Date.now();
  await fn();
  return Date.now() - inicio; // pode ser negativo
};

// Certo: duracao sempre pelo relogio monotonico. Ele nao diz que horas
// sao, mas garante que a diferenca entre duas leituras e o tempo
// decorrido de verdade.
export const medir = async (fn) => {
  const inicio = process.hrtime.bigint();
  await fn();
  return Number(process.hrtime.bigint() - inicio) / 1e6; // ms
};

// Prazos tambem sao duracao, nao data. Um prazo guardado como instante
// absoluto expira cedo demais ou tarde demais quando o relogio salta.
export const criarPrazo = (duracaoMs) => {
  const limite = process.hrtime.bigint() + BigInt(Math.round(duracaoMs * 1e6));
  return {
    expirado: () => process.hrtime.bigint() >= limite,
    restanteMs: () => Number(limite - process.hrtime.bigint()) / 1e6,
  };
};

// Guarda de sanidade para o unico lugar onde o relogio de parede e
// inevitavel: gravar quando o evento aconteceu no mundo real. Um carimbo
// que anda para tras dentro do mesmo processo e um salto detectado, e
// registrar isso e o que transforma um dado absurdo em um alerta.
let ultimoCarimbo = 0;

export const agoraMonotonicoAproximado = () => {
  const parede = Date.now();
  if (parede <= ultimoCarimbo) {
    // Salto para tras: mantem a ordem local avancando um milissegundo e
    // deixa o desvio visivel para a metrica em vez de silencia-lo.
    ultimoCarimbo += 1;
    return { carimbo: ultimoCarimbo, saltoDetectadoMs: ultimoCarimbo - parede };
  }
  ultimoCarimbo = parede;
  return { carimbo: parede, saltoDetectadoMs: 0 };
};

A separação entre relógio de parede e relógio monotônico é a correção de maior retorno e a mais barata de aplicar. Toda medição de duração, todo prazo de expiração e todo cálculo de tempo restante deve usar o relógio monotônico, que só avança e não tem noção de data. Todo registro de quando algo aconteceu no mundo real usa o relógio de parede, porque é o único que tem significado fora do processo. A confusão entre os dois é o que produz bloqueios distribuídos que expiram no instante em que são criados, e essa classe de falha não aparece em nenhum teste porque exige que o salto aconteça dentro da janela de execução.

UsoRelógio corretoO que quebra com o relógio errado
Medir quanto tempo uma chamada levouMonotônicoDuração negativa e métrica de latência corrompida
Expirar um bloqueio distribuídoMonotônico no detentor, parede no árbitroDois detentores simultâneos sem erro registrado
Registrar quando o pedido foi criadoParedeData sem significado fora do processo
Ordenar eventos entre serviçosNenhum dos dois, usar contador lógicoInversão silenciosa sob desvio
Decidir qual escrita é mais recenteVersão da entidadeEscrita apagada sem conflito reportado

03

Ordenar sem relógio: o contador lógico por entidade

A solução para a ordenação não é sincronizar melhor os relógios, é parar de depender deles. O que o sistema precisa quase sempre não é saber que horas o evento aconteceu, e sim saber qual evento veio antes do outro. Essas duas perguntas são diferentes, e a segunda tem uma resposta exata que não custa nada: um contador que incrementa a cada evento daquela entidade. Dois eventos do mesmo pedido são comparáveis pelo contador com precisão absoluta, independentemente de qual máquina os produziu e de quanto os relógios discordam.

O ponto que costuma travar a adoção é o medo de precisar de um contador global, que seria um gargalo. Ele não é necessário. A ordenação global raramente é requisito de negócio: ninguém precisa saber se o pedido do cliente A veio antes do pedido do cliente B. O que importa é a ordem dentro de cada entidade, e essa ordem pode ser mantida por um contador local àquela entidade, que já é serializada pelo próprio banco no momento da atualização. O custo é uma coluna e uma condição no UPDATE.

// events/sequence.js
// Ordem por contador logico da entidade. Nao depende de relogio nenhum e
// nao exige coordenacao global: o contador e local ao agregado e o banco
// ja serializa as atualizacoes da mesma linha.

export const registrarEvento = async ({ db, entidadeId, tipo, dados }) => {
  // A sequencia vem do proprio banco, na mesma transacao que grava o
  // evento. Ler o valor atual na aplicacao e incrementar em memoria
  // reintroduz a corrida que o contador existe para eliminar.
  const { rows } = await db.query(
    `INSERT INTO eventos (entidade_id, sequencia, tipo, dados, registrado_em)
     VALUES (
       $1,
       COALESCE((SELECT MAX(sequencia) FROM eventos WHERE entidade_id = $1), 0) + 1,
       $2,
       $3,
       now()
     )
     RETURNING sequencia`,
    [entidadeId, tipo, dados],
  );

  return rows[0].sequencia;
};

// O consumidor detecta lacuna sem precisar de tempo: se recebeu a
// sequencia 7 e a ultima aplicada foi 5, a 6 esta em transito ou se
// perdeu, e aplicar a 7 agora corrompe o estado.
export const aplicarEmOrdem = ({ ultimaAplicada, evento, pendentes }) => {
  if (evento.sequencia <= ultimaAplicada) {
    return { acao: 'ignorar', motivo: 'duplicata ou reentrega' };
  }

  if (evento.sequencia > ultimaAplicada + 1) {
    pendentes.set(evento.sequencia, evento);
    return { acao: 'aguardar', lacuna: evento.sequencia - ultimaAplicada - 1 };
  }

  // Aplicou o proximo esperado: drena o que ja chegou fora de ordem.
  let cursor = evento.sequencia;
  const aplicar = [evento];
  while (pendentes.has(cursor + 1)) {
    cursor += 1;
    aplicar.push(pendentes.get(cursor));
    pendentes.delete(cursor);
  }

  return { acao: 'aplicar', eventos: aplicar, ultimaAplicada: cursor };
};

A propriedade que esse desenho entrega e que nenhum carimbo entrega é a detecção de lacuna. Com carimbos de tempo, um evento perdido é indistinguível de um intervalo em que nada aconteceu, e o consumidor aplica o evento seguinte sobre um estado incompleto sem nenhum sinal. Com contador, a lacuna é aritmética: a sequência sete chegando depois da cinco significa que a seis está em trânsito ou se perdeu, e essa é a diferença entre um sistema que detecta a falha em segundos e um que descobre a inconsistência semanas depois numa conciliação.

04

Última escrita vence é uma política de perda de dados

A estratégia mais difundida para resolver escritas concorrentes é comparar os carimbos e manter o maior. Ela é atraente porque não exige coordenação e não gera erro para o cliente, e é exatamente por isso que é perigosa: o dado perdido não produz nenhum sintoma. Sob dessincronia, a escrita descartada não é a mais antiga, é a que veio da máquina cujo relógio estava atrasado, e essa escolha é arbitrária em relação à intenção do usuário.

O cenário concreto é banal. Dois atendentes atualizam o mesmo cadastro com dois segundos de diferença. O primeiro escreve pela instância cujo relógio adianta cem milissegundos; o segundo, pela instância cujo relógio atrasa duzentos. A escrita do segundo atendente, que é a correta e a mais recente na realidade, carrega um carimbo menor e é descartada pela regra. O cliente vê o dado antigo, ninguém recebe erro, e o log registra as duas escritas como bem-sucedidas. Investigar isso semanas depois é impossível, porque não há nenhum registro de que uma decisão de descarte aconteceu.

// storage/write.js
// Substitui 'ultima escrita vence por carimbo' por versao da entidade.
// A comparacao passa a ser sobre o que o cliente leu, nao sobre em qual
// maquina o carimbo foi gerado.

export const atualizar = async ({ db, id, campos, versaoLida }) => {
  // A condicao na versao e o que transforma sobrescrita silenciosa em
  // conflito explicito: o UPDATE so afeta a linha se ninguem escreveu
  // entre a leitura do cliente e este momento.
  const { rowCount, rows } = await db.query(
    `UPDATE clientes
        SET dados = dados || $2::jsonb,
            versao = versao + 1,
            atualizado_em = now()
      WHERE id = $1 AND versao = $3
      RETURNING versao, dados`,
    [id, JSON.stringify(campos), versaoLida],
  );

  if (rowCount === 0) {
    const atual = await db.query(
      'SELECT versao, dados FROM clientes WHERE id = $1',
      [id],
    );

    if (atual.rowCount === 0) {
      return { status: 'ausente' };
    }

    // Conflito real: alguem escreveu no intervalo. Devolver o estado
    // atual permite ao chamador decidir entre mesclar campo a campo,
    // repetir sobre a versao nova ou perguntar ao usuario. Nenhuma
    // dessas opcoes existe quando o carimbo decide sozinho.
    return {
      status: 'conflito',
      versaoAtual: atual.rows[0].versao,
      dadosAtuais: atual.rows[0].dados,
      camposEmConflito: Object.keys(campos).filter(
        (campo) => campo in atual.rows[0].dados,
      ),
    };
  }

  return { status: 'gravado', versao: rows[0].versao, dados: rows[0].dados };
};

Quando o requisito realmente exige convergência automática sem erro para o cliente, a saída não é voltar ao carimbo de parede: é usar uma estrutura em que a mesclagem é definida pelo tipo do dado. Um contador vira soma de incrementos, um conjunto vira união com marcação de remoção, um campo de texto vira o valor da réplica de maior identificador em caso de empate. Todas essas regras são determinísticas e independentes de relógio, e a única coisa que elas pedem em troca é que a escolha de mesclagem seja explícita por campo, em vez de implícita e global.

05

Guardar os dois carimbos: um para ordenar, outro para auditar

A conclusão natural de tudo isso costuma virar um exagero: abandonar o carimbo de tempo. Isso não funciona, porque existem perguntas legítimas que só o relógio de parede responde. Quanto tempo o cliente esperou de verdade, se o evento aconteceu dentro do horário comercial, se a retenção de trinta dias já venceu, qual foi a hora do incidente no relatório para o cliente. Nenhuma dessas perguntas é respondida por um contador lógico, que não tem relação com o tempo humano.

O desenho que resolve os dois lados é guardar os dois campos com papéis separados e nomes que não se confundem. A sequência lógica é o que o código usa para ordenar, comparar e detectar lacuna. O carimbo de parede é o que a interface exibe e o relatório usa, com a ressalva explícita de que ele é aproximado. Quando os dois discordam, e eles vão discordar, a sequência ganha, porque ela é a única com garantia. E a discordância em si vira um sinal: registrar o desvio observado entre a ordem lógica e a ordem temporal é o que transforma o problema de relógio em uma métrica em vez de um chamado de suporte.

// events/record.js
// Grava os dois carimbos com papeis distintos e mede a discordancia
// entre eles. A metrica resultante e o que revela dessincronia antes de
// ela virar dado absurdo na tela do cliente.

export const construirRegistro = ({ sequencia, anterior, origem }) => {
  const carimbo = new Date().toISOString();

  // Se o evento anterior da mesma entidade tem sequencia menor mas
  // carimbo maior, os relogios das duas origens discordam pelo menos
  // nessa diferenca. E a unica forma de medir desvio sem instalar agente
  // em cada maquina: usar a ordem causal ja conhecida como referencia.
  const desvioObservadoMs =
    anterior && anterior.sequencia < sequencia
      ? Math.max(0, Date.parse(anterior.carimbo) - Date.parse(carimbo))
      : 0;

  return {
    // Autoridade para ordenar, comparar e detectar lacuna.
    sequencia,
    // Aproximacao para exibir, filtrar por periodo e aplicar retencao.
    // Nunca usado para decidir precedencia.
    carimbo,
    origem,
    desvioObservadoMs,
  };
};

// O consumidor da metrica so precisa do maximo por par de origens: e ele
// que define o intervalo abaixo do qual a ordem por carimbo e ficcao.
export const acumularDesvio = (acumulador, registro, origemAnterior) => {
  if (registro.desvioObservadoMs === 0) return acumulador;
  const chave = [origemAnterior, registro.origem].sort().join('|');
  const atual = acumulador.get(chave) ?? 0;
  acumulador.set(chave, Math.max(atual, registro.desvioObservadoMs));
  return acumulador;
};

Essa métrica tem uma vantagem operacional que costuma ser subestimada: ela mede o desvio como o sistema o experimenta, não como o daemon de sincronização o reporta. O NTP informa o erro estimado em relação ao servidor de referência, o que não captura o caso em que duas máquinas estão sincronizadas com fontes diferentes, cada uma dentro da própria tolerância, e ainda assim discordam entre si na soma dos erros. Usar a ordem causal já conhecida como referência mede exatamente a grandeza que quebra o sistema.

06

Os três alertas que pegam a dessincronia antes do dado absurdo

Monitorar relógio costuma se resumir a um painel com o desvio reportado pelo NTP em cada host, que é o sinal menos útil dos disponíveis: ele é verde em todos os casos em que o problema é de configuração e não de sincronização, como fuso horário errado no contêiner ou relógio virtualizado que o daemon nem gerencia. Três sinais derivados do comportamento do próprio sistema cobrem o que importa.

O primeiro é a inversão observada entre ordem lógica e ordem temporal, medida em milissegundos e agregada por par de origens. Ele responde diretamente a pergunta operacional que interessa, que é qual intervalo mínimo entre eventos ainda é confiável. O segundo é o salto para trás dentro de um mesmo processo, contado pela guarda de sanidade: qualquer ocorrência é anômala e indica correção abrupta ou máquina retomada de suspensão. O terceiro é a duração negativa em qualquer medição, que só é possível quando alguém mediu tempo com relógio de parede e é o indicador mais direto de que o código tem a mistura que causa expiração incorreta de bloqueio.

SinalDetectaFalha se usado sozinhoDestino
Inversão entre sequência e carimboDesvio real entre pares de serviçosSó aparece onde já existe contador lógicoPainel e revisão semanal
Salto de relógio para trás no processoCorreção abrupta ou máquina retomadaNão vê desvio constante entre máquinasChamado imediato
Duração negativa em mediçãoUso de relógio de parede para medir tempoSilencioso enquanto nenhum salto ocorreTarefa no dia
Desvio reportado pelo NTPFalha de sincronização declaradaVerde em fuso errado e relógio virtualizadoPainel apenas

A última recomendação é de configuração e vale mais do que qualquer alerta: o daemon de sincronização deve corrigir o relógio acelerando ou desacelerando gradualmente, nunca dando um salto, exceto no momento da inicialização da máquina, antes de qualquer processo da aplicação subir. Com essa configuração, o relógio nunca anda para trás durante a execução, e a classe inteira de falhas por salto desaparece sem uma linha de código. O que sobra é o desvio, que é ruído previsível, e para ele a resposta já está dada: não ordene por carimbo, ordene por sequência.

FAQ

Perguntas frequentes

Sincronizar todos os relógios com precisão de microssegundos não resolveria o problema?

Reduz a frequência, mas não muda a natureza da falha, e o custo de chegar perto disso é alto o suficiente para não compensar na maioria dos sistemas. Sincronização de altíssima precisão exige hardware e caminhos de rede dedicados, do tipo usado em bolsas de valores, e mesmo lá a garantia é de erro limitado, não de erro zero. O ponto que costuma ser esquecido é que o intervalo entre eventos concorrentes diminui conforme o sistema cresce: sob carga, duas atualizações da mesma entidade ficam separadas por microssegundos, e nenhuma precisão prática de relógio ordena isso de forma confiável. Há também um caso que a precisão não toca: o relógio de contêiner que herda o host errado, o servidor que volta de suspensão e a máquina virtual migrada ao vivo produzem discrepância grande independentemente de quão preciso o daemon é. A abordagem que escala é assumir que a ordem entre máquinas não é observável pelo tempo e resolver a ordenação onde ela realmente pode ser garantida, que é dentro de cada entidade. Investir em sincronização continua valendo para carimbos de auditoria e correlação de logs, onde erro de dezenas de milissegundos é aceitável.

Como corrigir dados que já foram gravados com ordem invertida?

O primeiro passo é medir a extensão antes de tocar em qualquer linha, e a medida certa não é quantos registros têm carimbo suspeito, e sim quantos têm carimbo dentro da janela de desvio máximo em relação ao registro adjacente da mesma entidade. Fora dessa janela, a ordem por carimbo continua correta e o dado não precisa de correção. Dentro dela, a ordem é indeterminada, e o segundo passo é procurar uma fonte independente de causalidade: identificador de rastreamento que liga requisição a resposta, referência explícita ao evento anterior no payload, número de tentativa, ou a ordem de inserção na chave primária quando ela é sequencial e gerada pelo banco. Essas fontes costumam resolver a maioria dos casos ambíguos sem adivinhação. O terceiro passo é decidir o que fazer com o resto, e a resposta honesta quase sempre é deixar como está e marcar: adicionar uma coluna indicando que a ordem daquele intervalo é incerta é mais útil do que reordenar por heurística, porque a marcação é auditável e a reordenação errada é indistinguível da correta. Reprocessar só compensa quando o efeito da inversão é visível para o cliente e o valor correto é derivável de outra fonte.

Contador lógico por entidade não vira gargalo em entidades com muita escrita?

Vira, e o limite prático é justamente a taxa de escrita serializada naquela linha, tipicamente algumas centenas por segundo antes de a contenção aparecer. A boa notícia é que esse limite é o mesmo que a entidade já tem por outros motivos: se duas escritas concorrentes precisam ser ordenadas entre si, elas já competem pelo mesmo estado, e o contador não adiciona serialização, apenas torna visível a que já existia. Quando a taxa realmente excede isso, o problema não é o contador, é a granularidade da entidade, e a correção é dividir o agregado em partes que possam evoluir independentemente. Um caso concreto é o histórico de mensagens de uma conversa muito ativa: usar a conversa como unidade de sequência cria contenção, enquanto usar o par conversa mais participante mantém a ordem que importa para a leitura sem serializar tudo. Existe ainda a alternativa de gerar identificadores ordenáveis com prefixo temporal e sufixo aleatório, que dispensa coordenação e mantém a ordenação aproximada, mas ela volta a depender do relógio para a parte grossa da ordem e só é adequada quando a inversão dentro da mesma janela de milissegundos é tolerável.

A ordem dos eventos precisa ser construída, não observada no relógio

Um carimbo de tempo diz quando uma máquina achou que era, não em que ordem as coisas aconteceram, e todo sistema distribuído que confunde as duas coisas acaba com um dado absurdo na tela do cliente. Posso revisar como o seu sistema ordena eventos e definir a separação entre relógio monotônico e de parede, o contador lógico por entidade que detecta lacuna, a substituição da regra de última escrita vence por versionamento com conflito explícito, o par de carimbos com papéis separados para ordenar e auditar, e os alertas que medem o desvio pela ordem causal em vez de confiar no que o daemon de sincronização reporta.