Long Polling no Node.js mantém uma requisição HTTP aberta até existir uma atualização ou até um timeout. Quando o servidor responde, o cliente abre outra requisição. A técnica oferece atualizações quase em tempo real em ambientes onde WebSocket ou Server-Sent Events não estão disponíveis.
O modelo continua usando HTTP convencional, proxies e autenticação já existentes. Em contrapartida, cada cliente pode manter uma conexão pendente, e a aplicação precisa controlar timeouts, cancelamento, memória, limites de concorrência e reconexões.
Fluxo básico
- o cliente envia
GET /updates?after=cursor; - o servidor consulta novidades;
- se existirem, responde imediatamente;
- se não existirem, aguarda um sinal ou timeout;
- o cliente processa e abre nova requisição.
Endpoint simples
app.get('/updates', async (req, res) => {
const after = String(req.query.after ?? '');
const userId = req.auth.userId;
const updates = await repository.findAfter({ userId, after });
if (updates.length > 0) {
return res.json({ items: updates, cursor: updates.at(-1).id });
}
return waitForUpdates({ req, res, userId, after });
});Aguardando com timeout
function waitForUpdates({ req, res, userId, after }) {
const timeout = setTimeout(async () => {
cleanup();
res.status(204).end();
}, 25_000);
const unsubscribe = eventBus.subscribe(userId, async () => {
const updates = await repository.findAfter({ userId, after });
if (updates.length === 0 || res.headersSent) return;
cleanup();
res.json({ items: updates, cursor: updates.at(-1).id });
});
function cleanup() {
clearTimeout(timeout);
unsubscribe();
}
req.on('close', cleanup);
}Sempre remova listeners quando o cliente desconecta ou o timeout ocorre. Caso contrário, a aplicação acumula memória e callbacks.
Não use apenas memória
Um EventEmitter local acorda somente clientes conectados à mesma instância. Em múltiplos processos ou pods, use um sinal compartilhado, como Redis Pub/Sub, broker ou PostgreSQL LISTEN/NOTIFY.
Consulte LISTEN e NOTIFY no Node.js.
Sinal versus estado
O sinal apenas informa que pode haver dados novos. O endpoint deve consultar uma fonte persistente usando o cursor. Assim, uma notificação perdida não perde a atualização.
Cursor estável
SELECT id, type, payload, created_at
FROM user_updates
WHERE user_id = $1
AND id > $2
ORDER BY id ASC
LIMIT 100;Use um ID monotônico ou cursor composto. Evite depender apenas de timestamp quando vários eventos podem ter o mesmo valor.
Cliente com reconexão
async function poll() {
let delay = 0;
while (!controller.signal.aborted) {
try {
if (delay) await sleep(delay, controller.signal);
const response = await fetch(`/updates?after=${cursor}`, {
signal: controller.signal
});
if (response.status === 204) {
delay = 0;
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
applyUpdates(data.items);
cursor = data.cursor;
delay = 0;
} catch (error) {
if (controller.signal.aborted) break;
delay = Math.min(30_000, Math.max(1_000, delay * 2 || 1_000));
}
}
}Use jitter para muitos clientes não reconectarem ao mesmo tempo.
Timeouts de proxy
O timeout da aplicação deve ser menor que o timeout de load balancer, CDN ou proxy. Se o proxy fecha em 30 segundos, responda por volta de 20 a 25 segundos para encerrar de forma controlada.
Keep-Alive
Long polling abre muitas requisições sequenciais. HTTP Keep-Alive reduz custo de novas conexões. Consulte HTTP Keep-Alive no Node.js.
Limites por usuário
Um cliente defeituoso pode abrir várias polls simultâneas. Limite uma ou poucas conexões por usuário, sessão e IP.
if (activePolls.count(userId) >= 2) {
return res.status(429).json({ error: 'Muitas requisições pendentes' });
}Libere o contador em todos os caminhos de cleanup.
Autorização
Valide o acesso antes de esperar. O cursor não pode permitir que um usuário consulte eventos de outro. Inclua tenant e identidade na query.
Backpressure
Retorne lotes limitados. Se houver mais dados, o cliente repete imediatamente com o novo cursor.
{
"items": [ ... ],
"cursor": "18482",
"hasMore": true
}Não envie milhares de eventos em uma única resposta.
Eventos duplicados
O cliente pode receber o mesmo item após timeout ou falha de rede. Use IDs e processamento idempotente.
Cancelamento
Quando o cliente fecha a conexão, cancele consultas e operações relacionadas. O evento close ajuda, mas uma query em andamento pode exigir AbortSignal ou cancelamento do driver.
Graceful shutdown
Durante deploy, pare de aceitar novas polls e responda às existentes com 503 e Retry-After, ou encerre-as com resposta vazia para reconexão.
for (const response of activeResponses) {
if (!response.headersSent) {
response.setHeader('Retry-After', '2');
response.status(503).end();
}
}Long Polling versus SSE
Server-Sent Events mantém uma única resposta e envia vários eventos, reduzindo overhead. Long polling funciona melhor quando infraestrutura interrompe streams longos ou quando a frequência de atualização é baixa.
Veja Server-Sent Events no Node.js.
Long Polling versus WebSocket
WebSocket oferece comunicação bidirecional persistente. Long polling é mais simples para atualizações ocasionais do servidor ao cliente e usa semântica HTTP normal.
Consulte WebSocket com Node.js.
Cache
Respostas de atualização por usuário não devem ser armazenadas por caches compartilhados.
res.setHeader('Cache-Control', 'no-store');Observabilidade
Monitore polls ativas, tempo médio de espera, respostas com dados, timeouts vazios, desconexões, reconexões, erros e número de listeners. Um aumento repentino pode indicar loop de reconexão ou proxy encerrando cedo.
Testes
- dados existentes respondem imediatamente;
- novo evento acorda a poll;
- timeout retorna 204;
- cliente fecha conexão;
- listener é removido;
- duas instâncias recebem sinal;
- cursor evita perda;
- evento duplicado;
- limite por usuário;
- shutdown durante espera.
Erros comuns
- manter listeners após desconexão;
- usar apenas EventEmitter local;
- não persistir atualizações;
- timeout maior que o proxy;
- não limitar conexões;
- retornar lotes ilimitados;
- usar timestamp instável;
- não tratar duplicação;
- cachear respostas privadas.
Checklist
- cursor durável;
- sinal compartilhado;
- timeout controlado;
- cleanup completo;
- limite por usuário;
- lotes pequenos;
- reconexão com backoff;
- autorização aplicada;
- no-store;
- shutdown gracioso.
Conclusão
Long Polling no Node.js oferece atualizações quase em tempo real usando HTTP convencional. A técnica é útil quando SSE e WebSocket não são opções ou quando eventos são pouco frequentes.
Use uma fonte persistente, cursor estável e um sinal compartilhado apenas para acordar a requisição. Com timeout, cleanup e limites por usuário, long polling pode operar de forma previsível mesmo em múltiplas instâncias.
Consulte a documentação HTTP do Node.js e o RFC de semântica HTTP.



