Temporizadores fazem parte de praticamente toda aplicação de servidor. Eles controlam atrasos, intervalos, timeouts, tentativas, tarefas periódicas e operações que devem continuar apenas depois de uma determinada etapa do ciclo de eventos. O módulo de Timers no Node.js fornece APIs simples, mas o comportamento real depende do event loop, da carga do processo e da forma como callbacks assíncronos são organizados.
Um timer não garante que o código será executado exatamente no instante informado. Ele define o menor atraso antes que o callback possa entrar na fila correspondente. Se a thread principal estiver ocupada, a execução ocorrerá mais tarde. Entender essa diferença é essencial para evitar cronômetros imprecisos, filas de callbacks e serviços que não encerram corretamente.
Neste guia, você aprenderá a usar setTimeout(), setInterval(), setImmediate(), as versões baseadas em Promises, cancelamento com AbortSignal, ref(), unref(), medição de atrasos e padrões seguros para produção.
Como os timers funcionam?
O Node.js executa JavaScript em uma thread principal e coordena operações assíncronas através do event loop. Quando você registra um timer, o runtime armazena o callback e o atraso mínimo. Depois que esse prazo termina, o callback pode ser processado na fase apropriada, desde que o event loop esteja livre.
A documentação oficial de Timers descreve as funções globais, os objetos retornados e a API de Promises. Para revisar a plataforma, consulte o que é Node.js e o que é JavaScript.
Usando setTimeout()
setTimeout() agenda uma função para depois de um atraso mínimo:
const timeout = setTimeout(() => {
console.log('Executado depois de aproximadamente 2 segundos');
}, 2000);O valor retornado é um objeto Timeout. Ele pode ser usado para cancelar, atualizar ou alterar a influência do timer no ciclo de vida do processo.
Cancelando um timeout
const timeout = setTimeout(() => {
console.log('Esta mensagem não será exibida');
}, 5000);
clearTimeout(timeout);Guarde a referência quando a operação puder ser concluída antes do prazo. Cancelar timers desnecessários reduz callbacks acumulados e evita efeitos depois que uma requisição já terminou.
Criando um intervalo
setInterval() repete o callback enquanto não for cancelado:
let executions = 0;
const interval = setInterval(() => {
executions += 1;
console.log('Execução', executions);
if (executions === 5) {
clearInterval(interval);
}
}, 1000);Intervalos não são ideais quando a tarefa pode demorar mais do que o período. Uma nova execução pode ficar pendente enquanto a anterior ainda está processando banco de dados, rede ou arquivos.
Intervalo seguro com timeout recursivo
Para evitar sobreposição, agende a próxima execução somente depois que a atual terminar:
async function runPeriodicTask() {
try {
await synchronizeData();
} catch (error) {
console.error('Falha na sincronização', error);
} finally {
setTimeout(runPeriodicTask, 5000);
}
}
runPeriodicTask();Esse padrão cria uma pausa real entre as execuções. Em produção, acrescente cancelamento e limite de falhas. O artigo sobre retry com backoff no Node.js apresenta estratégias para tentativas controladas.
setImmediate() e o event loop
setImmediate() agenda um callback para a fase check do event loop. Ele é útil para dividir um trabalho extenso e devolver o controle ao runtime:
setImmediate(() => {
console.log('Executado em uma próxima iteração do event loop');
});A ordem entre setTimeout(fn, 0) e setImmediate(fn) pode variar dependendo do contexto. Dentro de certos callbacks de I/O, setImmediate() costuma ser processado antes do timer de atraso zero. Não baseie uma regra de negócio em uma disputa entre essas duas filas.
queueMicrotask e process.nextTick
Microtarefas não são timers, mas aparecem em comparações de ordem:
console.log('início');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
queueMicrotask(() => console.log('microtask'));
process.nextTick(() => console.log('nextTick'));
console.log('fim');Callbacks de process.nextTick() e microtarefas são processados antes de o event loop avançar para outras fases. Um loop que adiciona novas tarefas continuamente pode impedir timers e I/O de progredirem. Use essas APIs para pequenas continuações, não para processamento ilimitado.
Timers baseados em Promises
O módulo node:timers/promises facilita o uso com async e await:
const { setTimeout: delay } = require('node:timers/promises');
async function main() {
await delay(1000);
console.log('Um segundo depois');
}
main();Renomear a função para delay evita confusão com a versão global baseada em callback.
Cancelamento com AbortController
const { setTimeout: delay } = require('node:timers/promises');
const controller = new AbortController();
setTimeout(() => controller.abort(), 500);
try {
await delay(5000, null, {
signal: controller.signal
});
} catch (error) {
if (error.name === 'AbortError') {
console.log('Espera cancelada');
}
}O cancelamento impede que uma operação continue apenas porque um atraso foi iniciado. Veja também o guia de AbortController no Node.js.
Usando ref() e unref()
Por padrão, um timer ativo mantém o processo Node.js em execução. Com unref(), o timer deixa de impedir o encerramento:
const cleanupTimer = setInterval(() => {
removeExpiredEntries();
}, 60000);
cleanupTimer.unref();Se não houver outro trabalho ativo, o processo pode terminar sem esperar esse intervalo. ref() restaura o comportamento padrão. Esse recurso é útil para telemetria, limpeza opcional e tarefas auxiliares, mas não deve ser aplicado a uma operação obrigatória.
Atualizando um timeout com refresh()
O método refresh() reinicia a contagem sem criar outro objeto:
const inactivity = setTimeout(disconnectClient, 30000);
function activityDetected() {
inactivity.refresh();
}Esse padrão pode controlar inatividade em conexões. Ao encerrar o cliente, cancele o timeout para não manter referências desnecessárias.
Timers não são relógios exatos
Uma tarefa síncrona longa atrasa todos os callbacks:
const startedAt = Date.now();
setTimeout(() => {
console.log('Atraso real:', Date.now() - startedAt);
}, 100);
const end = Date.now() + 1000;
while (Date.now() < end) {
// Bloqueia a thread principal
}O callback solicitado para 100 milissegundos executará somente depois que o loop síncrono terminar. Para cálculos pesados, use processos ou Worker Threads, em vez de esperar que timers resolvam o bloqueio.
Medindo event loop lag
Uma forma simples de observar atrasos é comparar o horário esperado com o horário real:
const intervalMs = 1000;
let expected = Date.now() + intervalMs;
setInterval(() => {
const now = Date.now();
const lag = now - expected;
console.log({ lag });
expected = now + intervalMs;
}, intervalMs);Em sistemas reais, utilize métricas do runtime e monitore percentis. Picos frequentes podem indicar CPU saturada, callbacks síncronos, garbage collection ou excesso de tarefas.
Timeouts em chamadas externas
Não confunda um timer isolado com o cancelamento da operação. Este padrão é incompleto:
await Promise.race([
fetch(url),
delay(5000).then(() => {
throw new Error('Timeout');
})
]);A Promise rejeita, mas a requisição pode continuar consumindo recursos. Prefira enviar um AbortSignal para a operação. Também cancele o timer quando a chamada terminar, evitando trabalho residual.
Agendamento persistente
Timers existem apenas na memória do processo. Se o servidor reiniciar, o agendamento desaparece. Para tarefas importantes, use uma fila persistente, um agendador externo ou cron do ambiente. O conteúdo sobre BullMQ no Node.js explica jobs armazenados no Redis.
Encerramento da aplicação
Durante o shutdown, pare intervalos, cancele timeouts e aguarde tarefas críticas:
let stopping = false;
const interval = setInterval(processBatch, 10000);
process.on('SIGTERM', async () => {
if (stopping) return;
stopping = true;
clearInterval(interval);
await closeResources();
process.exitCode = 0;
});O guia de Graceful Shutdown no Node.js detalha sinais, servidores e recursos.
Erros comuns
- Esperar precisão absoluta: timers dependem da disponibilidade do event loop.
- Usar intervalo para tarefa lenta: execuções podem se acumular.
- Esquecer clearTimeout: callbacks continuam depois do encerramento lógico.
- Usar Promise.race sem cancelar: a operação externa permanece ativa.
- Criar timers por requisição sem limite: memória e callbacks crescem.
- Executar trabalho pesado no callback: outros timers e I/O ficam atrasados.
- Usar timer para job crítico: reinícios apagam o agendamento.
Boas práticas para produção
- Trate o atraso como mínimo, não como horário exato.
- Evite sobreposição de tarefas periódicas.
- Cancele timers quando o contexto terminar.
- Use AbortSignal para cancelar a operação real.
- Monitore event loop lag e duração dos callbacks.
- Aplique
unref()apenas em tarefas opcionais. - Use filas persistentes para jobs importantes.
- Inclua timers no fluxo de graceful shutdown.
- Teste com relógio falso quando apropriado.
- Registre timeouts e cancelamentos sem expor dados sensíveis.
Conclusão
Os Timers no Node.js oferecem muito mais que um simples atraso. Eles ajudam a coordenar timeouts, intervalos, inatividade, retries e divisão de trabalho, mas continuam subordinados ao event loop.
Ao cancelar timers corretamente, evitar intervalos sobrepostos, usar AbortSignal e escolher mecanismos persistentes para tarefas críticas, você reduz vazamentos, atrasos inesperados e operações abandonadas. O resultado é um serviço mais previsível e fácil de encerrar, testar e monitorar.




