Hospedar bot de WhatsApp 24/7 no Brasil (Baileys ou Venom)
Como hospedar um bot de WhatsApp 24/7 no Brasil com Baileys ou Venom: onde a sessão precisa morar para sobreviver ao restart, o risco real de bloqueio do número e o deploy com git push.
Quem já subiu um bot de Telegram acha que hospedar um bot de WhatsApp 24/7 é a mesma tarefa com outro nome. Não é. No Telegram você guarda um token numa variável de ambiente e acabou: o processo sobe, se autentica e responde. Com Baileys ou Venom não existe token. Existe uma sessão, criada quando você conecta o número lendo um QR code, e essa sessão é um conjunto de chaves que muda sozinho enquanto o bot vive. Se o seu processo não reencontrar essas chaves depois de um restart, você volta pro celular pra parear tudo de novo.
É por isso que tanto tutorial funciona no primeiro deploy e falha no segundo. Este guia cobre o caminho inteiro com o olho no que realmente quebra: o que Baileys e Venom são de verdade (e o risco de bloqueio que vem junto), como a escolha da biblioteca muda o seu build, onde a sessão precisa morar pra sobreviver ao container ser recriado, como parear sem QR num servidor sem tela, e como colocar isso no ar com git push, com a fatura em Real. Se você quer só o básico de processo que roda o tempo todo, o guia de bot do Telegram e Discord cobre esse terreno e este aqui segue de onde ele parou.
Antes de tudo: Baileys e Venom não são API oficial
Vamos começar pela parte que quase nenhum tutorial escreve. Baileys, Venom e whatsapp-web.js reimplementam o protocolo do WhatsApp Web. São projetos de código aberto que conversam com a infraestrutura do WhatsApp sem autorização da Meta, e a documentação do próprio Baileys diz isso com todas as letras: o projeto não é afiliado nem endossado pelo WhatsApp, e desencoraja uso para spam ou mensagem em massa (baileys.wiki).
Do outro lado, os Termos de Serviço do WhatsApp proíbem acessar ou usar o serviço de forma não autorizada, incluindo engenharia reversa e clientes modificados. A consequência prática é direta: o número pode ser bloqueado, sem aviso e sem prazo pra recorrer. A detecção é automatizada e não depende de alguém denunciar você.
Isso não significa que ninguém deva usar. Significa que a decisão precisa ser tomada com o risco na mesa:
- Use um número dedicado. Nunca o seu pessoal, nunca o número principal da empresa. Chip de bot é chip descartável.
- Volume humano. Responder quem falou com você é um perfil de tráfego. Disparar lista é outro, e é o que mais chama atenção do lado de lá.
- Se o bot é crítico pro negócio, o caminho sem risco de termos é a plataforma oficial (WhatsApp Business Platform, a Cloud API), que desde 1º de julho de 2025 cobra por mensagem entregue, por categoria de template e por país, no lugar do antigo modelo por conversa (documentação de preços da Meta).
E a nossa parte de honestidade: hospedagem boa não te protege de bloqueio. O que a infraestrutura resolve é o resto, que também é bastante coisa: ficar de pé, não perder a sessão, voltar sozinho quando cai e te dizer o que aconteceu.
O estado das bibliotecas, hoje
Números conferidos direto no registro do npm em 5 de agosto de 2026, porque tutorial envelhece e biblioteca de WhatsApp envelhece rápido:
| Biblioteca | Licença declarada no npm | Última versão publicada | Como conversa com o WhatsApp |
|---|---|---|---|
| baileys | MIT | 7.0.0-rc14 e 6.7.24, ambas em 29/07/2026 | WebSocket direto, sem navegador |
| whatsapp-web.js | Apache-2.0 | 1.34.7, em 24/04/2026 | Puppeteer, com Chromium por dentro |
| venom-bot | MIT | 5.3.0, em 23/11/2024 | Puppeteer, com Chromium por dentro |
Duas leituras que mudam decisão:
No Baileys, a tag latest do npm aponta hoje para uma release candidate. A linha estável 6.7.x vive na tag legacy. Ou seja, um npm i baileys sem versão te entrega o 7.0.0-rc14. Não é erro do mantenedor, é sinalização de transição, mas quem descobre isso em produção descobre do jeito ruim. Fixe a versão no package.json: "baileys": "6.7.24" se você quer previsibilidade, ou a 7.x assumida de propósito.
E tem um motivo mais duro que previsibilidade pra olhar essa linha do seu package.json agora: em junho de 2026 foi publicado um aviso de segurança crítico no Baileys (GHSA-qvv5-jq5g-4cgg, CVE-2026-48063), de spoofing de mensagem e corrupção do estado de sincronização via payload malicioso. Ele afeta tudo abaixo de 6.7.22 e as release candidates anteriores à 7.0.0-rc12. As duas versões da tabela acima já estão do lado corrigido, mas se você tem um projeto antigo com um 6.7.x qualquer travado, é hoje que ele sobe.
O venom-bot está sem release publicado desde novembro de 2024. Isso não quer dizer que parou de funcionar. Quer dizer que a distância entre a biblioteca e as mudanças do WhatsApp Web cresce sem ninguém encurtando, e essa conta chega em forma de plantão no seu fim de semana. Se você está começando um bot novo hoje, comece pelo Baileys.
A biblioteca escolhe o seu build (e a sua conta de memória)
Aqui está a diferença técnica que mais impacta o deploy: Baileys fala WebSocket, Venom e whatsapp-web.js sobem um navegador.
Com Baileys, o seu projeto é um projeto Node comum. O build usa o railpack, o builder zero-config que detecta Node pelo package.json, e você não escreve Dockerfile nenhum.
Com Venom ou whatsapp-web.js, tem um Chromium rodando dentro do container. Isso exige as bibliotecas de sistema que o Chromium precisa, o que na prática significa Dockerfile próprio (o caminho está detalhado no guia de deploy com Dockerfile). E exige memória: um serviço nasce com 0,5 vCPU e 512 MB no período de teste (1 vCPU e 1 GB no Starter), e Chromium nesse tamanho é aperto, não conforto. A saída é subir a memória do serviço no painel: um serviço pode usar até o total do plano, 2 vCPU e 2 GB no teste, 4 vCPU e 4 GB no Starter, 8 vCPU e 8 GB no Plus e 16 vCPU e 16 GB no Pro.
Resumindo a escolha em uma linha: se você não tem um motivo específico pra precisar do navegador, Baileys sai mais leve, cabe num plano menor e é mais simples de operar.
O bot é um worker, e o segredo dele não é estático
Um bot não é um site. Não escuta porta, não responde HTTP, não tem health check pra devolver 200. É um processo de longa duração que abre a conexão de saída e fica esperando eventos. Na plataforma isso se declara em uma linha, no upuai.toml:
#:schema https://upuai.com.br/schemas/upuai-toml-v1.json
[processes.bot]
command = "node bot.js"
type = "worker"
instances = 1
Até aqui, igualzinho a um bot de Telegram. A diferença aparece no segredo.
No Telegram, o token nasce no BotFather, você põe numa variável de ambiente e ele fica lá parado pelo resto da vida. No WhatsApp, as credenciais nascem no pareamento e o próprio processo as reescreve o tempo todo: o Baileys emite um evento creds.update sempre que o estado muda. Variável de ambiente não serve pra guardar algo que a aplicação precisa atualizar sozinha. Precisa de escrita.
A sessão não pode morar no disco do container
O exemplo padrão de todo tutorial usa useMultiFileAuthState('auth'), que grava as chaves numa pasta local. Funciona lindamente na sua máquina e é exatamente o que quebra em produção, por dois motivos independentes.
O primeiro é de infraestrutura: o disco do container é efêmero. Todo deploy monta um container novo a partir da imagem recém-construída, e o que estava escrito no sistema de arquivos do container antigo não vem junto. Um upuai redeploy e o QR volta.
O segundo é a própria biblioteca dizendo. A documentação do Baileys é explícita ao apresentar o useMultiFileAuthState: é “muito ineficiente e puramente para fins de demonstração”, com um “não dependa disso em produção” em caixa alta. O comentário no código-fonte da função vai na mesma direção e recomenda escrever um auth state próprio “para uso com um banco SQL ou NoSQL de verdade”.
Ou seja: mesmo com disco persistente, o caminho recomendado pela biblioteca é tirar o estado do sistema de arquivos. Então tiramos.
A sessão no Postgres, em um comando
Um banco gerenciado no mesmo projeto resolve o armazenamento:
upuai add --type database --engine postgres --yes
Isso provisiona um Postgres no mesmo projeto e injeta a DATABASE_URL no processo automaticamente, sem você copiar credencial de lugar nenhum. Se preferir Redis, é --engine redis e a variável vira REDIS_URL. A mecânica completa de banco gerenciado está no guia de PostgreSQL gerenciado.
Do lado do código, um auth state remoto é uma adaptação direta do exemplo oficial, trocando arquivo por linha de tabela. A tabela é chave e valor:
create table if not exists wa_auth (
id text primary key,
data text not null
);
Essa tabela precisa existir antes do bot subir, senão o primeiro deploy morre com relation "wa_auth" does not exist. O jeito de garantir isso é a release phase: um comando que a plataforma roda antes de trocar a versão no ar, a cada deploy. Acrescente ao upuai.toml:
[deploy]
releaseCommand = "node migrate.js"
E o migrate.js é do tamanho da tarefa:
import { Pool } from 'pg'
const db = new Pool({ connectionString: process.env.DATABASE_URL })
await db.query(`create table if not exists wa_auth (
id text primary key,
data text not null
)`)
await db.end()
O if not exists deixa isso idempotente: roda em todo deploy e não faz nada depois da primeira vez. Se a release phase falhar, o rollout nem começa, e o bot antigo continua no ar.
Prefere aplicar na mão, uma vez só? psql "$(upuai db connect --print)" -f schema.sql resolve, com a ressalva de que aí é você quem lembra de rodar isso em cada ambiente novo.
E o adaptador, em auth-pg.js:
import { initAuthCreds, BufferJSON, proto } from 'baileys'
export async function usePgAuthState(db, sessao) {
const chave = (id) => `${sessao}:${id}`
const ler = async (id) => {
const r = await db.query('select data from wa_auth where id = $1', [chave(id)])
return r.rows[0] ? JSON.parse(r.rows[0].data, BufferJSON.reviver) : null
}
const gravar = (id, valor) =>
db.query(
'insert into wa_auth (id, data) values ($1, $2) on conflict (id) do update set data = excluded.data',
[chave(id), JSON.stringify(valor, BufferJSON.replacer)]
)
const apagar = (id) => db.query('delete from wa_auth where id = $1', [chave(id)])
const creds = (await ler('creds')) || initAuthCreds()
return {
state: {
creds,
keys: {
get: async (tipo, ids) => {
const saida = {}
await Promise.all(
ids.map(async (id) => {
let valor = await ler(`${tipo}-${id}`)
if (tipo === 'app-state-sync-key' && valor) {
valor = proto.Message.AppStateSyncKeyData.fromObject(valor)
}
saida[id] = valor
})
)
return saida
},
set: async (dados) => {
const tarefas = []
for (const tipo in dados) {
for (const id in dados[tipo]) {
const valor = dados[tipo][id]
tarefas.push(valor ? gravar(`${tipo}-${id}`, valor) : apagar(`${tipo}-${id}`))
}
}
await Promise.all(tarefas)
},
},
},
saveCreds: () => gravar('creds', creds),
}
}
É pouco código, e ele é seu, não mágica da plataforma. O BufferJSON existe justamente porque as chaves são buffers binários e precisam sobreviver ao JSON.stringify sem virar lixo.
A ressalva honesta: se a sessão vive no banco, a durabilidade da sessão é a durabilidade do banco. No período de teste o storage de banco é de 1 GB e o backup automático é zero (a plataforma escreve isso na própria página de preços). Starter guarda 1 dia, Plus guarda 2, Pro guarda 3 e Business guarda 7. Perder a linha de credenciais não é catástrofe, mas é um pareamento manual no celular e o bot parado até alguém fazer isso.
Parear sem QR: o código de 8 caracteres
Servidor não tem tela, e QR code renderizado em log é uma péssima experiência: quebra o desenho, expira em segundos e some no scroll. O Baileys tem o caminho certo pra isso, o pairing code:
sock.ev.on('connection.update', async ({ qr }) => {
if (!qr || sock.authState.creds.registered) return
const codigo = await sock.requestPairingCode(process.env.WA_NUMERO)
console.log('codigo de pareamento:', codigo)
})
O número vai em formato E.164 sem o sinal de mais (por exemplo 5531999999999). O retorno é um código de 8 caracteres, que o WhatsApp mostra no formato XXXX-XXXX. No celular: WhatsApp, Aparelhos conectados, Conectar com número de telefone. Repare que o pedido só acontece quando o socket já emitiu o QR e o número ainda não está registrado: pedir antes disso não funciona.
Para ler o código em produção, o caminho é o log do processo:
upuai logs -n 100 --process bot # o código de pareamento sai aqui
Detalhe que assusta na primeira vez: logo depois do pareamento, o WhatsApp derruba a conexão de propósito. O Baileys sinaliza isso como DisconnectReason.restartRequired e o seu código só precisa reconectar. Não é bug, é o protocolo.
O deploy, do zero ao ar
O bot.js mínimo, já com sessão no banco e reconexão tratada:
import makeWASocket, { DisconnectReason } from 'baileys'
import { Pool } from 'pg'
import { usePgAuthState } from './auth-pg.js'
const db = new Pool({ connectionString: process.env.DATABASE_URL })
async function iniciar() {
const { state, saveCreds } = await usePgAuthState(db, 'principal')
const sock = makeWASocket({ auth: state })
sock.ev.on('creds.update', saveCreds)
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection !== 'close') return
const motivo = lastDisconnect?.error?.output?.statusCode
if (motivo === DisconnectReason.loggedOut) {
console.error('sessao encerrada no celular: precisa parear de novo')
process.exit(1)
}
setTimeout(iniciar, 3000) // espera antes de reconectar, nunca em loop apertado
})
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
if (msg.key.remoteJid === 'status@broadcast') return // status alheio nao se responde
await sock.sendMessage(msg.key.remoteJid, { text: 'no ar, 24/7' })
})
}
iniciar()
Duas linhas ali não são preciosismo. O setTimeout antes de reconectar evita o loop apertado de reconexão quando o WhatsApp está instável, e reconexão em rajada é exatamente o comportamento anômalo que pesa contra você do outro lado. E ignorar status@broadcast impede que o bot responda ao status que os contatos publicam, que é constrangedor e é padrão de tráfego de robô.
O upuai.toml fechado, com o processo e a release phase, fica assim:
#:schema https://upuai.com.br/schemas/upuai-toml-v1.json
[processes.bot]
command = "node bot.js"
type = "worker"
instances = 1
[deploy]
releaseCommand = "node migrate.js"
Com ele no repositório, o resto é a rotina da plataforma:
# macOS / Linux
brew tap saiph-ti/upuai-cli
brew install upuai
upuai login # uma vez por máquina
upuai init \
--name meu-bot-wa \
--repo sua-org/meu-bot-wa \
--branch main \
--framework Node.js \
--yes
upuai add --type database --engine postgres --yes
upuai variables set WA_NUMERO=5531999999999 --scope runtime --yes
upuai deploy --wait --yes
O --scope runtime mantém a variável fora da imagem de build, que é o escopo certo pra qualquer coisa sensível. A partir daqui, todo git push na branch conectada rebuilda e sobe o bot sozinho, e upuai rollback --to <id> --yes reverte se uma versão nova quebrar. Se ainda não há repositório git, upuai up empacota o diretório atual e deploya direto (excluindo .git, node_modules e .env*).
Uma instância, sempre
Essa regra é mais dura no WhatsApp do que em qualquer outro bot: nunca escale um bot de WhatsApp para mais de uma réplica. A sessão pertence a um aparelho conectado. Duas réplicas com as mesmas credenciais disputam o mesmo slot, a conexão fica instável, e conexão instável é justamente o tipo de comportamento anômalo que aumenta o risco do lado do WhatsApp. Deixe instances = 1 no upuai.toml e resista ao upuai scale.
Precisa de mais capacidade? O caminho é mais números, cada um com a sua própria sessão (uma chave diferente na tabela), nunca a mesma sessão replicada.
Logs, restart e os três jeitos de quebrar
O dia a dia cabe em quatro comandos:
upuai ps # processos do serviço
upuai logs -n 200 --process bot # runtime do bot
upuai logs --build # quando o problema foi o build
upuai restart --process bot --yes # reinicia só o bot
A retenção de log muda por plano: 12 horas no período de teste, 24 no Starter, 48 no Plus, 72 no Pro. Bot que cai na madrugada de sábado e é investigado no domingo à noite já perdeu a evidência com 12 horas de retenção, e essa é a razão mais concreta pra não ficar no menor plano quando o bot passa a importar.
Os três modos de falha que aparecem de verdade:
- Sessão encerrada no celular. Alguém entrou em Aparelhos conectados e desconectou. Chega como
DisconnectReason.loggedOute não adianta reconectar em loop: precisa parear de novo. - Celular principal sumido por tempo demais. Pela Central de Ajuda do WhatsApp, ficar 14 dias sem abrir o app no celular principal desconecta os aparelhos vinculados. O bot cai sem ninguém ter mexido em nada.
- Exceção engolida. Um
try/catchglobal que silencia erro transforma um crash visível num zumbi invisível. Deixe o processo morrer: o orquestrador o recoloca de pé e o motivo fica no log. Quando quem falha é o build, o erro chega com a causa e a ação recomendada, não com log cru.
Quanto custa em Real
| Plano | Preço/mês | Serviços por projeto | CPU / RAM do workspace | Storage de banco | Backup automático | Logs |
|---|---|---|---|---|---|---|
| Teste (15 dias) | grátis, sem cartão | 2 | 2 vCPU / 2 GB | 1 GB | nenhum | 12h |
| Starter | R$ 19 | 3 | 4 vCPU / 4 GB | 3 GB | 1 dia | 24h |
| Plus | R$ 49 | 4 | 8 vCPU / 8 GB | 6 GB | 2 dias | 48h |
| Pro | R$ 99 | 5 | 16 vCPU / 16 GB | 10 GB | 3 dias | 72h |
Nos 15 dias de teste cabe exatamente o combo deste guia: um serviço para o bot e um para o Postgres da sessão, sem cartão de crédito. Pra manter o bot no ar depois disso, o Starter é o degrau, e já traz backup diário da sessão e mais retenção de log. O Pro é território de quem roda Chromium, vários números ou fila em volta do bot.
No plano anual, os pagos saem por 10 vezes o valor mensal, o que dá 2 meses grátis. A cobrança é em Real, sem cartão internacional, sem IOF e sem câmbio na fatura (a matemática de por que isso pesa está no comparativo de custo de cloud no Brasil).
Perguntas frequentes
Meu número pode ser bloqueado?
Pode. Baileys, Venom e whatsapp-web.js são clientes não oficiais, e os Termos de Serviço do WhatsApp proíbem acesso não autorizado ao serviço. A detecção é automatizada e o bloqueio pode vir sem aviso. Reduza a exposição com número dedicado, volume humano e nada de disparo em massa. Garantia não existe, e quem promete garantia está te vendendo alguma coisa.
Preciso deixar o celular ligado o tempo todo?
Durante a operação, não: o modo multi-dispositivo mantém o aparelho conectado funcionando mesmo com o celular offline. Mas o celular principal precisa dar sinal de vida de tempos em tempos. Pela Central de Ajuda do WhatsApp, 14 dias sem abrir o app no celular derrubam os aparelhos vinculados, e aí o bot precisa parear de novo.
Dá pra rodar dois números no mesmo projeto?
Dá, com um processo por número e uma sessão por número. O detalhe que decide isso: variável de ambiente é por serviço, então os dois processos enxergam o mesmo WA_NUMERO. Se o número e a chave de sessão vierem hardcoded, você acabou de criar duas sessões idênticas brigando pelo mesmo slot, que é o problema que este guia inteiro tenta evitar. Passe os dois como argumento no command de cada processo:
[processes.principal]
command = "node bot.js principal 5531999999999"
type = "worker"
instances = 1
[processes.suporte]
command = "node bot.js suporte 5531888888888"
type = "worker"
instances = 1
No bot.js, process.argv[2] vira a chave passada ao usePgAuthState e process.argv[3] o número do pareamento. No período de teste são 2 processos por serviço e 2 serviços por projeto, então dois bots mais o banco cabem sem folga nenhuma. O terceiro número já pede o Starter.
E se eu precisar do Venom por causa de um recurso específico?
Aí você troca o builder para dockerfile no upuai.toml e leva o Chromium junto na imagem, seguindo o guia de Dockerfile. Conte com mais memória e mais tempo de build: suba a memória do serviço no painel antes de ir para produção, até o total do seu plano.
Como faço backup da sessão?
Se a sessão vive no Postgres, o backup do banco é o backup da sessão. Além da retenção automática por plano, upuai db backup --out backup.dump gera um dump sob demanda, e upuai db restore -f backup.dump --yes traz de volta. Guarde a cópia fora do servidor.
Do QR ao 24/7
O resumo do caminho: escolha a biblioteca sabendo se ela precisa de navegador, declare o bot como worker, tire a sessão do sistema de arquivos e coloque num banco gerenciado, pareie por código em vez de QR, mantenha uma instância só e leia o log quando cair. O bot passa a viver num container gerenciado no Brasil, que fica de pé, volta sozinho e conta o que aconteceu, com a fatura em Real.
O que a infraestrutura não faz, e nenhuma faz, é te dar imunidade aos termos do WhatsApp. Essa parte é decisão sua, tomada com o risco à vista.
Comece seus 15 dias grátis, sem cartão →
Referências úteis: a documentação do upuai.toml tem o esquema completo de processos e a documentação da CLI a lista inteira de comandos.
Faça deploy no Brasil em 5 minutos
PaaS brasileira, preços em Real, infraestrutura LGPD-native.
Ver planosArtigos relacionados
Bot de WhatsApp na Cloud API: quando sair da biblioteca não oficial
A conta da API oficial do WhatsApp muda em 1º de outubro de 2026: mensagem de serviço e utilidade dentro da janela de 24h passam a ser cobradas. Quando migrar e o que muda no deploy.
Baileys, Venom ou API oficial: qual usar para bot de WhatsApp
Compare Baileys, Venom e Cloud API para seu bot de WhatsApp. Entenda hospedagem, mensagens e as mudanças de cobrança anunciadas pela Meta para outubro de 2026.
Hospedar bot do Telegram ou Discord 24/7 no Brasil (Node e Python)
Guia prático para hospedar bot do Telegram ou Discord 24/7 no Brasil: rode como worker no Upuai com git push, token em variável de ambiente, logs por processo e custo em Real, começando grátis.