Blog

Como eu enxergo a arquitetura do WhatsApp.

Quarto texto da série em que eu leio arquiteturas conhecidas a partir de material público. Não é a planta: é a leitura do que organiza o resto.

Esse é o caso que mais me interessa dos seis, porque ele inverte uma intuição comum: a restrição de segurança mais dura do conjunto é também o que deixa o sistema mais simples.

O servidor não pode ler nada

Com criptografia ponta a ponta, a mensagem é cifrada no aparelho de quem envia e só é decifrada no de quem recebe. O servidor transporta bytes que não consegue interpretar.

Isso apaga, de uma vez, uma lista de coisas que outros produtos fazem no servidor:

Busca de mensagem no lado do servidor. Impossível. A busca vive no aparelho, sobre o histórico local.

Prévia de conteúdo, moderação automática do texto, recomendação baseada no que foi dito, anúncio por assunto da conversa. Nada disso existe.

Recuperar histórico ao trocar de aparelho. O servidor não tem o histórico em claro, então isso vira um problema de transferência entre dispositivos ou de backup cifrado com chave que o usuário controla.

Parece uma lista de limitações. Eu leio como um enxugamento: o servidor tem uma responsabilidade só, que é entregar bytes para o destinatário certo.

Store and forward, e depois esqueça

O modelo de entrega é o mais simples possível.

Aparelho A cifra aqui bytes opacos Servidor fila por destinatário guarda até entregar apaga depois do recibo bytes opacos Aparelho B decifra aqui recibo de entrega → o servidor descarta a mensagem O ESTADO PERMANENTE VIVE NOS APARELHOS. O SERVIDOR É UM CORREDOR.
Store and forward — guardar é exceção temporária, não função do sistema

O servidor guarda a mensagem cifrada enquanto o destinatário está offline, e descarta assim que a entrega é confirmada.

Isso tem uma consequência de custo que eu acho impressionante: o armazenamento não cresce com o uso. Um sistema que guarda todo o histórico de todos os usuários para sempre tem um custo que sobe monotonamente. Um que guarda só o que ainda não foi entregue tem um custo proporcional a quantas pessoas estão offline agora, que é um número quase constante.

A base de dados permanente é o aparelho de cada um. O servidor é infraestrutura de trânsito.

O problema real: conexões, não requisições

Mensageria não é tráfego HTTP comum. Cada aparelho mantém uma conexão aberta, esperando, por horas.

Isso muda o gargalo. Não é CPU por requisição: é quantas conexões ociosas uma máquina aguenta, cada uma consumindo um pouco de memória e um descritor de arquivo.

É por isso que a escolha de Erlang aparece em toda conversa sobre esse sistema, e ela não é folclore. A plataforma foi construída para telecomunicações: processos leves aos milhões, isolamento entre eles, supervisão que reinicia o que quebrou sem derrubar o resto.

O modelo mental encaixa quase perfeitamente. Cada conexão é um processo minúsculo e independente. Um erro em uma conversa não contamina as outras. E o número de usuários por servidor fica em uma ordem de grandeza incomum, o que explica a relação famosa entre o tamanho do time e o tamanho da base.

Eu tiro uma lição disso que não é sobre Erlang: a natureza do gargalo deveria escolher a tecnologia. Um sistema dominado por conexões ociosas tem restrições diferentes de um dominado por processamento. Escolher a linguagem por gosto e depois lutar contra o gargalo é o caminho comum, e é o mais caro.

Grupo, e o problema de N dispositivos

Aqui a criptografia cobra um preço.

Sem cifra, mandar mensagem pra um grupo de cem pessoas é uma escrita e um fan-out no servidor. Com cifra ponta a ponta, cada destinatário tem uma chave diferente, então o conteúdo precisaria ser cifrado uma vez para cada.

Pior: cada pessoa tem vários aparelhos, e cada aparelho é um destinatário criptográfico independente. Cem pessoas com três aparelhos são trezentas cifragens, feitas no celular de quem enviou.

A saída conhecida é uma chave de remetente para o grupo: quem envia cifra o conteúdo uma vez com essa chave, e distribui a chave individualmente para cada participante. A distribuição da chave é O(N), a mensagem é O(1). Enquanto ninguém entrar nem sair, as mensagens seguintes saem baratas.

Isso explica por que entrar e sair de grupo é caro e por que grupo tem limite de tamanho. O limite não é de banco de dados: é do custo de redistribuir chave.

Escrevi sobre a diferença entre garantir conteúdo e garantir origem em assinatura digital e hash — é a mesma família de decisões, aqui aplicada no caminho quente do produto.

O que a restrição obriga a fazer bem

Um sistema que não pode reler a mensagem depois precisa acertar a entrega na primeira vez.

Isso força identificador de mensagem estável gerado no cliente, para deduplicar reenvio. Força recibo em camadas, com estados distintos para enviado, entregue e lido. Força ordenação que não dependa do relógio do servidor, porque o servidor não é a fonte da verdade do conteúdo.

Tudo isso é idempotência e ordenação de eventos, os mesmos problemas de qualquer fila, só que com uma restrição a mais: quem coordena não pode inspecionar o que está coordenando.

O que eu levo pra sistema pequeno

Restrição forte simplifica. Decidir que o servidor não lê o conteúdo elimina dezenas de funcionalidades possíveis e, com elas, dezenas de subsistemas. Vale perguntar, em qualquer projeto, o que a gente ganharia se decidisse não guardar algo.

Estado permanente no cliente é uma opção legítima. Nem todo dado precisa viver no servidor. Quando o cliente é a fonte, o custo de armazenamento para de crescer com o tempo.

Deixe o gargalo escolher a tecnologia. Conexões ociosas, processamento pesado e escrita intensa são problemas diferentes, e as ferramentas boas em um costumam ser medianas nos outros.

Não guarde o que já foi entregue. É a política de retenção mais simples que existe, e quase ninguém considera, porque guardar parece sempre a opção segura. Guardar é responsabilidade, é custo, e é a primeira coisa que vaza.

← Voltar pro blog