O que quebra primeiro em um sistema de dados em tempo real?
Quando “tempo real” começa a dar errado
Quando alguém olha para uma tela de trading, o comportamento parece simples:
- o preço muda;
- o livro de ofertas se movimenta;
- novas negociações aparecem;
- os candles continuam sendo desenhados.
Só que mostrar dados em tempo real não significa apenas abrir um WebSocket e atualizar a interface sempre que uma mensagem chega.
Na prática, os problemas começam justamente quando alguma coisa deixa de funcionar perfeitamente.
A conexão cai.
Uma mensagem chega fora de ordem.
O navegador fica alguns segundos em segundo plano.
O cliente não consegue processar os eventos na mesma velocidade em que eles chegam.
E, de repente, uma interface que parecia correta começa a mostrar um estado que não corresponde mais ao servidor.
1. Receber dados não significa ter o estado correto
Imagine um order book.
Ao abrir a página, o cliente precisa saber quais ofertas existem naquele momento.
Uma estratégia comum é receber primeiro um snapshot, algo como:
- preço 100: 2 unidades;
- preço 99: 5 unidades;
- preço 98: 1 unidade.
Depois disso, o servidor começa a enviar apenas alterações:
- nível 100 mudou para 1 unidade;
- nível 99 foi removido;
- surgiu uma nova oferta em 101.
Isso é muito mais eficiente do que enviar o livro inteiro a cada mudança.
O problema aparece quando uma dessas mensagens nunca chega.
Se o cliente recebe:
1540 → 1541 → 1543
há uma lacuna.
A atualização 1542 sumiu.
Continuar aplicando eventos depois disso pode produzir um estado que parece válido, mas já está incorreto.
Esse tipo de bug é particularmente ruim porque a aplicação não necessariamente quebra.
Ela continua funcionando — apenas mostra informação errada.
2. Sequence IDs são mais importantes do que parecem
Por isso, muitos streams em tempo real incluem algum tipo de número de sequência.
O cliente mantém o último número processado e valida o próximo evento.
Se recebeu 1541, espera 1542.
Caso apareça 1543, existe evidência de que alguma informação foi perdida.
Nesse momento, existem algumas opções:
- ignorar a inconsistência;
- tentar recuperar apenas o evento ausente;
- descartar o estado local;
- pedir um novo snapshot ao servidor.
Para sistemas em que consistência importa, a quarta opção costuma ser uma das mais seguras.
Ela custa mais dados, mas evita manter silenciosamente um estado corrompido.
3. Reconectar não é simplesmente abrir outro WebSocket
Outro erro comum é pensar que, quando a conexão cai, basta executar novamente:
connect
A questão é:
o que aconteceu enquanto o cliente estava offline?
O mercado continuou mudando.
Ordens podem ter sido executadas.
Saldos podem ter sido alterados.
Posições podem ter mudado.
Se o cliente simplesmente reconectar e continuar de onde acha que parou, pode carregar uma visão antiga do sistema.
Uma reconexão mais robusta geralmente precisa fazer algo parecido com:
- detectar a desconexão;
- aguardar antes de tentar novamente;
- reconectar;
- recuperar o estado atual;
- comparar identificadores ou versões;
- só então voltar a aplicar eventos incrementais.
Em outras palavras, a reconexão é também um processo de ressincronização.
4. Backoff evita transformar um problema em dois
Agora imagine milhares de clientes desconectando ao mesmo tempo.
Talvez o servidor tenha acabado de reiniciar.
Se todos tentarem reconectar imediatamente, podem gerar uma nova carga justamente no momento em que a infraestrutura está tentando se recuperar.
Por isso, uma estratégia comum é usar exponential backoff.
Em vez de tentar novamente a cada 100 ms, o cliente espera progressivamente mais:
- 1 segundo;
- 2 segundos;
- 4 segundos;
- 8 segundos.
Normalmente também se adiciona um pouco de aleatoriedade, chamada de jitter, para evitar que milhares de clientes tentem reconectar exatamente no mesmo instante.
É um detalhe pequeno que faz muita diferença em escala.
5. E se o cliente for mais lento que o servidor?
Esse é outro problema interessante.
Suponha que o servidor envie 500 atualizações por segundo.
O navegador consegue processar 500?
Talvez.
Mas e se, para cada mensagem, a aplicação:
- atualizar o estado global;
- recalcular vários valores;
- renderizar componentes;
- atualizar um gráfico;
- reorganizar o DOM?
Nesse caso, a interface pode começar a acumular trabalho.
A rede continua funcionando perfeitamente, mas o cliente fica cada vez mais atrasado.
Depois de alguns segundos, o que aparece na tela pode estar centenas de eventos atrás do servidor.
Isso é um problema de backpressure.
6. Nem toda mensagem precisa gerar uma renderização
Uma solução é separar duas coisas:
frequência de entrada dos dados
e
frequência de atualização visual
O sistema pode receber centenas de eventos por segundo, atualizar o estado interno e redesenhar a interface apenas algumas dezenas de vezes por segundo.
Se o preço mudou:
100.01 → 100.02 → 100.03 → 100.04 → 100.05
em poucos milissegundos, talvez o usuário não precise ver cinco renderizações diferentes.
Mostrar diretamente o estado mais recente pode ser suficiente.
Isso reduz trabalho no frontend sem necessariamente perder informação relevante.
7. Dados públicos e dados privados seguem fluxos diferentes
Dados públicos são relativamente simples.
Preço, trades e order book podem ser distribuídos para milhares de usuários.
Mas uma plataforma de trading também possui streams privados:
- ordens do usuário;
- saldo;
- posições;
- depósitos;
- retiradas;
- alterações na conta.
Esses canais precisam de autenticação.
E aí aparecem outras perguntas:
- O que acontece quando o token expira?
- O WebSocket precisa ser recriado?
- É possível renovar a autenticação sem fechar a conexão?
- Eventos privados e públicos usam a mesma infraestrutura?
- Uma desconexão deve bloquear temporariamente certas ações da interface?
O frontend precisa distinguir:
“não houve nenhuma alteração”
de
“eu não sei se houve alteração porque perdi a conexão”
Essa diferença é muito importante.
8. “Sem atualização” e “sem conexão” não são a mesma coisa
Imagine que o saldo exibido seja 1.000.
Se nenhuma mensagem nova chegou nos últimos 30 segundos, isso pode significar duas coisas:
- o saldo continua em 1.000;
- a conexão caiu e o cliente simplesmente não sabe o saldo atual.
Visualmente, os dois estados podem parecer iguais.
Uma boa aplicação precisa conseguir distinguir entre eles.
Por isso, indicadores de conexão, timestamps de última atualização e mecanismos de heartbeat podem ser úteis.
9. Heartbeat ajuda a descobrir conexões que parecem vivas
Às vezes uma conexão não fecha explicitamente.
Ela simplesmente para de transmitir dados.
Isso pode acontecer por:
- proxies;
- redes móveis;
- roteadores;
- mudanças de conexão;
- timeout intermediário.
Uma técnica comum é enviar mensagens periódicas de ping/pong.
Se o cliente não receber resposta dentro de determinado intervalo, considera a conexão inválida e inicia a recuperação.
Sem isso, a aplicação pode passar minutos acreditando que ainda está conectada.
10. O que testar além do “happy path”?
O caminho feliz costuma ser simples:
conecta → recebe evento → atualiza tela
Mas os testes mais interessantes são outros.
Por exemplo:
- perder uma mensagem no meio da sequência;
- receber duas vezes o mesmo evento;
- receber eventos fora de ordem;
- ficar offline durante 20 segundos;
- reconectar com um snapshot diferente;
- receber 10 vezes mais eventos que o normal;
- suspender a aba do navegador;
- trocar de Wi-Fi para rede móvel;
- expirar a autenticação durante a sessão.
Esses casos revelam muito mais sobre a robustez de um sistema em tempo real do que apenas verificar se o WebSocket conecta.
11. Observabilidade também deveria existir no frontend
Quando algo dá errado, “o gráfico ficou estranho” não ajuda muito a descobrir a causa.
Algumas métricas podem ser bastante úteis:
- tempo desde o último evento;
- número de reconexões;
- diferença entre sequence IDs;
- tamanho da fila de eventos;
- latência entre timestamp do servidor e processamento no cliente;
- quantidade de eventos descartados;
- duração média de cada renderização.
Com essas informações fica mais fácil descobrir se o problema está:
- na rede;
- no servidor;
- no processamento local;
- ou simplesmente na renderização.
Tempo real é mais sobre recuperação do que sobre velocidade
Talvez essa seja a parte mais contraintuitiva.
Um sistema de dados em tempo real não é robusto porque consegue entregar uma mensagem rapidamente quando tudo está funcionando.
Ele é robusto quando consegue detectar que deixou de estar correto e voltar para um estado confiável.
Latência importa.
Mas também importam:
- ordenação;
- consistência;
- recuperação;
- controle de fluxo;
- observabilidade;
- reconexão.
Quando tudo isso funciona bem, o usuário vê apenas números mudando na tela.
E provavelmente nem imagina quantos mecanismos estão trabalhando para garantir que aqueles números ainda sejam verdadeiros.