2

Date.now() não é cronômetro: use o relógio certo

Uma requisição começa às 10:00:00.900 e termina 200 ms depois.

No log, a latência aparece como -800 ms.

Não é bug no sinal de menos. O relógio do servidor foi corrigido entre as duas leituras, e o código usou um relógio de parede como cronômetro.

O código certo em 99,99% dos casos

Já escrevi variações deste helper mais vezes do que gostaria:

async function medir(operacao) {
  const inicio = Date.now();
  const resultado = await operacao();

  return {
    resultado,
    duracaoMs: Date.now() - inicio,
  };
}

Ele funciona no teste, no notebook e durante meses em produção. Aí o sistema operacional sincroniza o horário, uma VM volta de um estado estranho ou chega um segundo bissexto. O segundo Date.now() pode ser menor que o primeiro.

Date.now() responde “qual é o instante atual desde a época Unix segundo esta máquina?”. Esse valor precisa acompanhar o horário civil, então pode receber correção. O sistema pode adiantá-lo, atrasá-lo ou alterar temporariamente a velocidade com que avança.

Timestamp é coordenada. Duração é distância. Subtrair coordenadas de uma régua ajustável dá uma distância falsa.

Dois relógios moram na mesma máquina

Para código de aplicação, vale guardar este modelo mental:

PerguntaRelógioExemplo em JavaScript
“Quando aconteceu?”ParedeDate.now(), new Date()
“Quanto demorou?”Monotônicoperformance.now()

O relógio de parede tem uma origem compartilhada e vira data ISO. É o que permite juntar logs de serviços diferentes. O monotônico começa em um ponto arbitrário e garante que a próxima leitura não será menor que a anterior.

parede:     1000 ── 1001 ── 0998 ── 0999   correção
monotônico:   40 ───── 41 ───── 42 ───── 43

No Linux, CLOCK_REALTIME é o relógio ajustável. CLOCK_MONOTONIC não sofre saltos para trás, embora ajustes graduais de frequência possam alterar seu ritmo. Já CLOCK_BOOTTIME inclui o tempo em que a máquina ficou suspensa.

Aqui mora uma confusão comum: resolução, precisão e monotonicidade são propriedades diferentes. Um relógio com microssegundos pode estar errado e voltar. Um relógio monotônico mais grosseiro ainda é o cronômetro correto.

Segundo a documentação da MDN, o navegador pode oferecer resolução de 5 µs em contexto isolado ou reduzi-la para 100 µs por privacidade. Nos dois casos, performance.now() continua monotônico.

Quando uma duração negativa derrubou DNS

Na virada de 2016 para 2017, um segundo bissexto acionou exatamente esse tipo de falha no RRDNS da Cloudflare.

O serviço media o desempenho de resolvedores DNS upstream. Algumas durações ficaram negativas. O número entrou no cálculo de escolha ponderada do resolver e chegou a rand.Int63n, do Go, que dispara panic quando recebe argumento negativo.

O postmortem da Cloudflare registrou cerca de 0,2% das consultas DNS afetadas no pico e menos de 1% das requisições HTTP. Eram poucas máquinas, mas espalhadas por 102 data centers. Os pontos mais afetados receberam correção em 90 minutos; o rollout global acabou às 06:45 UTC.

Redundância não salvou o sistema porque o gatilho era correlacionado. O mesmo evento externo encontrou a mesma hipótese falsa em várias réplicas ao mesmo tempo.

Depois dessa classe de problema, o Go passou a carregar duas leituras dentro de time.Time: uma de parede e, quando disponível, uma monotônica. Operações como Sub usam a parte monotônica. A proposta da mudança estimou que cerca de 30% das chamadas a time.Now encontradas no código eram usadas para medir duração.

Não era uma curiosidade sobre calendários. Era uma armadilha de API aparecendo em produção.

Troque o cronômetro e preserve o orçamento

Dentro do mesmo contexto do navegador ou processo Node.js, a correção é pequena:

async function medir(operacao) {
  const inicio = performance.now();
  const resultado = await operacao();

  return {
    resultado,
    duracaoMs: performance.now() - inicio,
  };
}

A Performance API usa um relógio monotônico relativo a performance.timeOrigin. No Node.js, performance.now() conta milissegundos de alta resolução desde o início do processo. Para nanossegundos inteiros, existe process.hrtime.bigint().

A mesma regra melhora timeouts. Imagine que a rota inteira tem 250 ms para autenticação, banco e um serviço externo. Dar 250 ms novos para cada etapa transforma um orçamento em até 750 ms.

Crie um deadline monotônico e passe o saldo adiante:

async function buscarComDeadline(url, deadlineMs) {
  const restanteMs = Math.max(0, deadlineMs - performance.now());
  if (restanteMs === 0) throw new Error('orçamento esgotado');

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), restanteMs);

  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

const deadlineMs = performance.now() + 250;
await autenticar(deadlineMs);
await consultarBanco(deadlineMs);
await buscarComDeadline('https://estoque.interno', deadlineMs);

Timeout limita uma etapa; deadline conserva o orçamento da operação inteira.

Antes de iniciar trabalho novo, verifique se ainda existe saldo. Registre também qual etapa o consumiu. “Timeout no banco depois de usar 187 dos 250 ms” é investigável; “a requisição expirou” quase não ajuda.

Onde o monotônico deixa de servir

Um valor monotônico só faz sentido em relação à origem daquele relógio. O 5421.3 do processo A não tem relação com o 5421.3 do processo B. Reiniciar o processo cria outra origem.

Por isso, não salve esse valor no banco e não o mande pela rede. O próprio Go remove a leitura monotônica ao serializar.

NecessidadeEscolha
Latência, backoff, timeout localRelógio monotônico
Data de log, nota fiscal, agendaRelógio de parede
expires_at persistidoHorário de uma autoridade, normalmente o servidor
Ordenar eventos entre máquinasSequência, relógio lógico ou ordem do banco
Segurança de lease distribuídoAutoridade/consenso e fencing token

Relógios monotônicos também divergem durante suspensão. Em Linux, CLOCK_MONOTONIC para de contar; CLOCK_BOOTTIME não. A documentação do Go alerta que o tempo suspenso pode ficar fora de uma duração. O requisito decide: “cinco minutos de processo ativo” e “cinco minutos sentidos pelo usuário” são coisas diferentes.

Sincronizar por NTP não apaga essas escolhas. O leap smear evita um salto espalhando a correção pela taxa do relógio, mas nem existe um padrão único. A proposta do Go descreve o Google distribuindo um segundo em 20 horas, com taxa de 99,9986%. A Meta usa uma janela de 17 horas.

Observabilidade precisa dos dois

No log, cada relógio deve cumprir uma função:

const observadoEm = new Date().toISOString();
const inicio = performance.now();

await atenderRequisicao();

logger.info({
  observadoEm,
  duracaoMs: performance.now() - inicio,
});

O timestamp de parede permite correlacionar serviços. A duração monotônica não absorve uma correção local. Monitore o offset do relógio da máquina como outra métrica; não o misture com latência.

Em regra de negócio, injete funções como agoraCivil() e agoraMonotonico(). No teste, faça o horário civil recuar, avançar e correr em outra taxa enquanto o monotônico segue controlado. Assim dá para quebrar hipóteses sobre expiração sem mexer no relógio do notebook.

Não aceite Math.max(0, Date.now() - inicio) como solução final. Limitar o valor a zero esconde a medição errada. A Cloudflare usou uma defesa contra negativos para mitigar o incidente, mas o conserto da classe de bug é escolher o relógio certo. Guardas continuam úteis como alarme.

A regra de uma linha

Parede responde quando. Monotônico responde quanto tempo.

Use Date para valores que pessoas e sistemas trocam. Use performance.now(), process.hrtime.bigint() ou a API monotônica da sua linguagem para latência, orçamento e deadline local. Nunca atravesse a fronteira do processo com a leitura monotônica.

Onde no seu código ainda existe uma subtração entre dois horários de parede? O que aconteceria se o segundo fosse menor?

Carregando publicação patrocinada...
2

O performance tem outros métodos úteis também, como mark e measure:

performance.mark("login-started");
performance.mark("login-finished");

const loginMeasure = performance.measure(
  "login-duration",
  "login-started",
  "login-finished",
);
console.log(loginMeasure.duration);

É útil para quando o contexto não é o mesmo, então não precisa passar o resultado de performance.now() de um lado para outro.