Notas de desenvolvimento · Confiabilidade
A sessão do WhatsApp que se desconectou 377 vezes
Uma sessão do WhatsApp Web no nosso produto desktop passou uma noite emitindo códigos QR. Trezentos e setenta e sete deles. Não havia ninguém ali para escanear nenhum, e cada ciclo tornava o seguinte mais provável.
A leitura óbvia, e a que qualquer artigo sobre bibliotecas não oficiais do WhatsApp vai lhe entregar, é que a Meta detectou automação e puniu. Não foi isso que aconteceu. O dispositivo foi desconectado porque onze processos Chromium nossos estavam vivos ao mesmo tempo, todos segurando o mesmo diretório de autenticação, todos educadamente tomando a sessão uns dos outros. A Meta fez a única coisa sensata disponível a ela.
Resumo
O SIGKILL é assíncrono. Sinalizar um processo de navegador e imediatamente subir o substituto roda os dois ao mesmo tempo com frequência, porque o sistema operacional ainda não coletou o primeiro nem liberou seus bloqueios de arquivo.
Navegadores vazados dividem um diretório de autenticação. O whatsapp-web.js enxerga uma sessão em conflito e invoca takeoverOnConflict. Com vários clientes seus vivos, eles tomam a sessão uns dos outros em loop.
A Meta responde desconectando o dispositivo. A autenticação some, e só uma pessoa escaneando um QR code restaura, então um loop de reconexão sem supervisão gera códigos para sempre e nunca se recupera.
Três correções: um encerramento que consulta os PIDs até confirmarem que sumiram, um mutex por sessão para que a limpeza sempre termine antes do próximo start, e uma política que estaciona a sessão em vez de gerar códigos QR para ninguém.
A cascata
Relendo o log, a falha tem uma ordem rígida. Cada passo é razoável isoladamente e a sequência é fatal.
LocalAuth, porque todos são o mesmo identificador de sessão.takeoverOnConflict contra o que são, na verdade, os nossos próprios zumbis.EBUSY, porque um navegador sinalizado mas ainda vivo o mantém aberto. Um diretório apagado pela metade é pior que qualquer um dos dois resultados.O ponto que vale parar para pensar é o passo 5. Passamos um tempo procurando o que tínhamos feito para acionar a detecção de automação da Meta, e a resposta era nada. A plataforma se comportou corretamente. A desconexão foi inteiramente autoinfligida, e nenhum padrão de envio mais suave ou atraso aleatório teria evitado.
Por que o encerramento não encerrava
A causa raiz é uma linha de suposição. Chamar process.kill(pid, 'SIGKILL') retorna imediatamente, e é tentador ler esse retorno como "o processo acabou". Não é. Significa que o sinal foi entregue. O sistema operacional pode levar centenas de milissegundos para coletar o processo, e até lá ele ainda segura seus descritores de arquivo, seu SingletonLock e sua parte do diretório de autenticação.
Então a correção não é um sinal melhor, é uma confirmação. Sinalize todos os PIDs candidatos e depois consulte até todos terem realmente sumido.
src/local-modules/wa-session-supervisor.mjs
const deadline = Date.now() + Math.max(pollIntervalMs, timeoutMs);
let survivors = targets;
while (Date.now() < deadline) {
survivors = survivors.filter((pid) => checkAlive(pid));
if (!survivors.length) return { confirmed: true, survivors: [] };
await sleep(pollIntervalMs);
}
Um detalhe em checkAlive é fácil de inverter. Sondar um processo com o sinal 0 lança erro quando o processo sumiu, mas também lança EPERM quando o processo existe e você não tem permissão para sinalizá-lo. Tratar todo erro como "morto" reintroduz exatamente o vazamento, então EPERM precisa ser lido como vivo:
try {
processKill(pid, 0);
return true;
} catch (error) {
// EPERM = existe mas não é sinalizável (ainda vivo); qualquer outro = sumiu.
return Boolean(error && error.code === 'EPERM');
}
Serializando o ciclo de vida
Um encerramento verificado só elimina o vazamento se nada iniciar enquanto ele roda. A invariante de que realmente precisávamos era mais forte que "encerre direito": no máximo um cliente vivo e um Chromium por identificador de sessão, e nenhum start novo até o navegador anterior estar confirmadamente morto.
Isso é um problema de exclusão mútua, com chave por sessão. Uma cadeia de promises simples é o atalho tentador e tem uma propriedade ruim: uma tarefa rejeitada contamina todas as tarefas na fila atrás dela, então uma única limpeza com falha travaria aquela sessão permanentemente. O lock, portanto, engole a falha da tarefa anterior mas ainda espera ela terminar:
await previous.catch(() => undefined);
try {
return await task();
} finally {
release();
if (chains.get(k) === gate) chains.delete(k);
}
As duas últimas linhas importam mais do que parecem. Sem remover a entrada quando a tarefa atual é a última da fila, o mapa de cadeias por sessão cresce por toda a vida do processo, o que é um vazamento lento introduzido pela correção de um rápido.
Recusando gerar códigos QR para ninguém
A última correção é uma política, não um mecanismo, e é a que teria nos ajudado primeiro. Depois que a Meta desconecta um dispositivo, a autenticação salva some. Nenhuma quantidade de tentativas reconecta. Só uma pessoa escaneando um QR code consegue, e se não houver ninguém presente o loop simplesmente roda até alguém perceber.
Então o supervisor agora estaciona sessões em vez de tentar para sempre. Duas regras, ambas derivadas do que o log de fato mostrou:
- Uma tempestade de desconexões estaciona na hora. Duas ou mais desconexões em dez minutos não é uma pessoa desvinculando o celular. São clientes brigando, e continuar reconectando piora.
- Uma janela de revinculação, depois estaciona. Após uma desconexão, a sessão ganha uma única janela de seis códigos QR não escaneados. Se nenhum for escaneado, ela vai para
needs_relinke para. Uma reconexão manual reinicia a janela, então o usuário nunca fica impedido de tentar de novo.
A propriedade importante é que needs_relink é um estado visível, e não uma parada silenciosa. Uma sessão que parou de tentar e diz isso se recupera em um minuto. Uma sessão gerando o seu 300º QR code parece ocupada e não está.
O que isso custa de verdade
Operamos os dois lados do mundo WhatsApp em um só produto. O aplicativo desktop conduz sessões whatsapp-web.js contra números pessoais, e o aplicativo em nuvem fala com a Cloud API oficial da Meta para números comerciais. Isso torna a comparação concreta em vez de retórica.
Nada da falha acima é possível na Cloud API. Não há navegador, nem diretório de autenticação, nem sessão de dispositivo, nem QR code, então não há o que vazar e nenhum dispositivo para desconectar. A troca é direta quando dita com clareza: as bibliotecas não oficiais não custam nada por mensagem e carregam uma superfície operacional que fica invisível até falhar às duas da manhã. A API oficial elimina essa superfície inteira e cobra por mensagem no lugar.
A discussão pública sobre bibliotecas não oficiais do WhatsApp é quase toda sobre risco de banimento, que é real. O que fica de fora é o custo corrente comum, e esta nota é uma noite dele. Se você está pesando os dois caminhos, calcule a supervisão que vai ter de escrever, não só as mensagens que não vai ter de pagar.
Usa whatsapp-web.js e está pensando na API oficial?
A Klaros opera os dois lados. O lado desktop mantém vivas as sessões de números pessoais, e o lado em nuvem envia pela Cloud API da Meta na sua própria conta do WhatsApp Business, cobrada diretamente pela Meta, sem margem por mensagem no meio do caminho. Seus contatos e grupos vão junto, porque a própria Klaros sincroniza isso. Seu histórico de conversas não vai, e nenhum fornecedor pode prometer o contrário com honestidade: a Meta não permite importar conversas anteriores para a Cloud API.
+91 97893 77634 · você fala primeiro, então nada chega até você sem a sua permissão.
Perguntas que as pessoas fazem sobre isso
Por que o whatsapp-web.js fica mostrando um novo QR code em vez de reconectar?
Um QR code que se repete significa que a autenticação salva foi perdida, não que a conexão está lenta. A causa habitual é mais de um processo de navegador segurando o mesmo diretório LocalAuth: o whatsapp-web.js enxerga uma sessão em conflito, invoca takeoverOnConflict, e os clientes brigam entre si até a Meta desconectar o dispositivo. Depois disso, só uma pessoa escaneando um QR code resolve, então um loop de reconexão automática vai gerar códigos indefinidamente sem nunca ter sucesso.
Por que a Meta desconectou meu dispositivo se eu não estava enviando nada?
Uma desconexão forçada nem sempre é a Meta detectando automação. Se dois ou mais processos Chromium seus estão vivos sobre um mesmo diretório de autenticação, eles tomam a sessão uns dos outros em sequência, e a Meta trata esse padrão como um dispositivo em que não pode confiar. No nosso caso havia onze navegadores vazados vivos ao mesmo tempo, e a desconexão foi inteiramente autoinfligida.
Chamar process.kill basta para encerrar um Chromium vazado?
Não. O SIGKILL é assíncrono: o sistema operacional pode levar centenas de milissegundos para coletar o processo e liberar seus bloqueios de arquivo, incluindo o diretório LocalAuth e o SingletonLock. Um código que sinaliza um processo e imediatamente sobe outro no lugar vai rodar os dois ao mesmo tempo com frequência. O encerramento precisa ser verificado, consultando o PID até ele realmente ter sumido.
O que significa um erro EBUSY no diretório de autenticação do WhatsApp?
Significa que algo ainda tem o diretório aberto, o que no Windows quase sempre é um processo de navegador que recebeu o sinal mas ainda não encerrou. Apagar o diretório de autenticação enquanto ele está preso ou falha, ou tem sucesso pela metade, e um diretório apagado pela metade produz exatamente o loop de QR code que a limpeza deveria resolver. Tentar a remoção de novo com um pequeno intervalo, depois de confirmar que os processos morreram, é a ordem confiável.
A API oficial do WhatsApp Cloud tem esse mesmo problema?
Não. A Cloud API é hospedada pela Meta e não mantém navegador, diretório de autenticação nem sessão de dispositivo, então não há nada para vazar e nenhum dispositivo para desconectar. Essa é a troca que se faz: as bibliotecas não oficiais não custam nada por mensagem e carregam essa superfície operacional, enquanto a Cloud API elimina a superfície e cobra por mensagem. A falha documentada aqui é um custo corrente do caminho não oficial que raramente entra na conta.
Escrito em 3 de setembro de 2026 a partir do server.log de 1–2 de julho de 2026. Atualizamos quando os fatos mudam.
