Presença é um sinal em tempo real que indica se um usuário está online, ausente ou offline, mantido fresco por conexões e heartbeats. A bolinha verde é uma interface enganosamente simples sobre um problema distribuído genuinamente difícil: o servidor precisa inferir a ausência — dispositivos raramente anunciam a própria morte — e depois transmitir cada inferência para todos que estão observando, na escala de uma lista de contatos. Padronizado muito antes do chat moderno, nas stanzas de presença do XMPP, o padrão tem fundamentos que não mudaram desde então.
Principais pontos
| Pergunta | Resposta |
|---|---|
| Os estados | online · ausente/ocioso · offline — mais overrides definidos pelo usuário (não perturbe) |
| O mecanismo | Escritas de heartbeat + expiração por TTL; eventos de conexão como caminho rápido |
| Os números | Heartbeat de ~30 s · timeout de offline em 2–3× o intervalo · 30–60 s de carência contra oscilação |
| O companheiro | Visto por último — o timestamp durável por trás da bolinha efêmera |
| A parte difícil | Fan-out: N observadores × M mudanças de status, multiplicados pelo seu sucesso |
O contrato do heartbeat
O padrão inteiro cabe em uma troca:
cliente a cada 30 s: save { status: "online", lastActiveAt: now }
servidor a cada 60 s: varredura — lastActiveAt mais velho que 90 s? → status: "offline"
(TTL = 2–3 × intervalo do heartbeat)
observador status = "online" → bolinha verde
status = "offline" → "visto às 12:41" ← lastActiveAt, a parte durável
O mesmo contrato como código de SDK — heartbeat para fora, subscription para dentro:
// JavaScript / Node.js — Back4app JS SDK
// Presence: heartbeat your own status, subscribe to your contacts'
const heartbeat = () =>
myPresence.save({ status: 'online', lastActiveAt: new Date() });
await heartbeat();
setInterval(heartbeat, 30_000); // server sweeps offline at 2–3× this
const contacts = new Parse.Query('Presence');
contacts.containedIn('user', myContactIds);
const sub = await contacts.subscribe();
sub.on('update', (p) => setStatus(p.get('user'), p.get('status'))); // Flutter / Dart — Back4app Flutter SDK
// Presence: heartbeat your own status, subscribe to your contacts'
Future<void> heartbeat() async {
myPresence
..set('status', 'online')
..set('lastActiveAt', DateTime.now());
await myPresence.save();
}
Timer.periodic(const Duration(seconds: 30), (_) => heartbeat());
final contacts = QueryBuilder<ParseObject>(ParseObject('Presence'))
..whereContainedIn('user', myContactIds);
final sub = await LiveQuery().client.subscribe(contacts);
sub.on(LiveQueryEvent.update, (p) => setStatus(p.get('user'), p.get('status'))); // iOS / Swift — Back4app Swift SDK
// Presence: heartbeat your own status, subscribe to your contacts'
func heartbeat() async throws {
myPresence.status = "online"
myPresence.lastActiveAt = Date()
_ = try await myPresence.save()
}
// Fire every 30 s — the server sweeps offline at 2–3× this
let contacts = Presence.query(containedIn(key: "user", array: myContactIds))
let sub = try await contacts.subscribe()
sub.handleEvent { _, e in
if case .updated(let p) = e { setStatus(p.user, p.status) }
} // Android / Kotlin — Back4app Android SDK
// Presence: heartbeat your own status, subscribe to your contacts'
fun heartbeat() {
myPresence.put("status", "online")
myPresence.put("lastActiveAt", Date())
myPresence.saveInBackground()
}
timer.scheduleAtFixedRate(0, 30_000) { heartbeat() } // sweep at 2–3× this
val contacts = ParseQuery.getQuery<ParseObject>("Presence")
contacts.whereContainedIn("user", myContactIds)
val sub = ParseLiveQueryClient.Factory.getClient().subscribe(contacts)
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, p ->
setStatus(p.getString("user"), p.getString("status"))
} Eventos de conexão vs. TTL de heartbeat
Os dois modelos de detecção, comparados com honestidade — nenhuma página de ranking faz isso em uma tabela só:
| Ciclo de vida da conexão | Heartbeat + TTL | |
|---|---|---|
| Sinal de online | Socket aberto | Heartbeat fresco dentro da janela |
| Sinal de offline | Evento/frame de fechamento | Expiração do TTL — sem heartbeat por 2–3 intervalos |
| Velocidade de detecção | Instantânea no fechamento limpo | Atraso limitado (até o timeout) |
| Falhas silenciosas | Não pega — queda de energia não envia frame de fechamento | Pega — o silêncio é o sinal |
| Estado no servidor | Rastreamento por conexão | Escritas sem estado em um store com TTL |
| Como falha | Usuários “online” zumbis | Falso “offline” breve em batidas perdidas |
O protocolo WebSocket em si ensina a lição: ele traz frames de controle ping/pong justamente porque o TCP não revela um par que sumiu, e sua taxonomia de close codes reserva o 1006 para “fechamento anormal — nenhum frame de fechamento recebido”, o caso zumbi. Presença de produção, portanto, empilha os dois modelos: eventos de conexão para transições instantâneas, expiração de heartbeat como a verdade que pega o que os eventos deixam passar.
A máquina de estados
Dois refinamentos distinguem as implementações bem-acabadas. Ausente é detectado no cliente: o servidor não enxerga uma aba em segundo plano, mas o navegador sim — a Page Visibility API sinaliza abas ocultas, listeners de entrada sinalizam ociosidade, e um sinal de offline em melhor esforço no fechamento da aba viaja pela Beacon API. Offline tem debounce: o estado de carência existe porque conexões móveis oscilam — publique “offline” a cada túnel e elevador e a lista de contatos de cada observador pisca em solidariedade. Atrase o anúncio, cancele na reconexão, e a oscilação colapsa em quietude. Mais uma distinção que vale roubar do protocolo que formalizou presença: presença computada (online, pela conexão) versus status definido pelo usuário (não perturbe) — o segundo sempre ganha o merge.
Efêmero por design
Presença é um cache da realidade, não um registro: um status que chega atrasado é pior que status nenhum — o “online” de ontem é mentira hoje. Esse princípio guia as escolhas de implementação: os status vivem em stores rápidos com TTL em vez de tabelas duráveis, nunca são enfileirados para entrega offline e são o primeiro dado descartado sob backpressure. Só o lastActiveAt merece persistência. Indicadores de digitação são o princípio no extremo — micro-presença escopada a uma conversa, expirando em segundos, com debounce no remetente e descartada em vez de retentada; se “Ada está digitando…” não pode chegar agora, não deveria chegar nunca.
O problema do fan-out
O muro de escala da presença é multiplicação, não armazenamento. Um exemplo real: 100.000 usuários simultâneos, cada um visível para 50 contatos, cada um mudando de estado modestos 20 vezes por hora — isso dá 100.000 × 50 × 20 = 100 milhões de eventos de presença por hora para entregar, vindos de uma feature que guarda uma linha pequena por usuário. As alavancas, na ordem em que se puxa: inscreva-se de forma estreita — observe os usuários que estão na tela (a lista de conversas abertas), não o grafo inteiro de contatos; puxe, não empurre, a cauda longa — busque o visto por último quando um perfil abrir, em vez de transmiti-lo; limite a transmissão em grupos — acima de algumas centenas de membros, mostre presença na interação, não para a lista toda; e agrupe transições para que um usuário oscilando custe um evento com debounce, não trinta. O próprio fan-out roda sobre a maquinaria padrão de pub/sub — presença é o caso de canto at-most-once desse padrão, em que descartar um evento velho é feature.
Vários dispositivos e privacidade
Um usuário, três dispositivos, uma bolinha — presença por usuário é um merge sobre presença por dispositivo. Rastreie o heartbeat de cada dispositivo separadamente; o usuário está online enquanto qualquer um estiver, e a política de merge é “o mais disponível vence”: online no celular ganha de ocioso no desktop, e o não-perturbe de qualquer dispositivo sobrepõe os demais. O visto por último reporta o dispositivo mais recente. Privacidade é a outra metade do design, e a metade que os blogs de engenharia pulam apesar de ser o que os usuários mais perguntam: regras de visibilidade (todos / contatos / ninguém), reciprocidade (esconda o seu, perca a visão do dos outros) e visto por último grosseiro (“recentemente” em vez de horário). Aplique as regras nas permissões da camada de dados — presença é dado comportamental, e um “online às 3 da manhã” vazado é uma divulgação real.
Casos de uso comuns
- Chat e mensageria — a bolinha verde canônica, o visto por último e os indicadores de digitação.
- Ferramentas de colaboração — quem está no documento, presença de cursor, barras laterais de “ativos agora”.
- Suporte e marketplaces — roteamento por disponibilidade de atendente, sinais de confiança do tipo “vendedor online”.
- Multiplayer e social — lobbies, listas de amigos, atalhos para entrar junto com um amigo.
- Ferramentas de força de trabalho — estados de disponibilidade alimentando roteamento e dashboards de status.
Como você deveria construir presença? Matriz de decisão
| Feature | Mecanismo |
|---|---|
| Bolinha verde nos contatos | Heartbeat + store com TTL, subscription de live query nos usuários visíveis |
| Transições instantâneas em um chat aberto | Eventos de conexão como caminho rápido no socket |
| Visto por último | Timestamp a cada heartbeat; busca sob demanda |
| Detecção de ausência | No cliente: listeners de visibilidade e ociosidade, reportados no heartbeat |
| Indicador de digitação | Micro-eventos por conversa, TTL de segundos, nunca persistidos |
| Presença em um grupo de 5.000 membros | Não transmita — mostre na interação, limite a lista |
Limitações e trade-offs
- Presença é probabilística. Entre heartbeats, a bolinha é um palpite; sistemas honestos abraçam a defasagem limitada em vez de fingir verdade instantânea.
- Frescor custa escritas. Reduzir o intervalo de heartbeat pela metade dobra a carga de escrita de toda a população online — velocidade de detecção se compra em throughput.
- Fan-out escala com o sucesso. A feature é barata com 1.000 usuários e vira projeto de arquitetura com 10 milhões; defina o escopo das subscriptions antes que o crescimento force.
- Oscilação é inerente. Redes móveis garantem churn de reconexão; sem debounce, a presença amplifica ruído de rede em ruído de interface.
- O formato é de vigilância. Padrões de horário online revelam sono, trabalho e hábitos; controles de visibilidade e aplicação na camada de permissões são requisitos, não melhorias.
Presença no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Os blocos de construção acima mapeiam um a um: uma classe Presence guarda status e lastActiveAt; o heartbeat é um save periódico (as abas de código); observadores mantêm uma subscription de Live Query nos contatos que estão na tela, recebendo atualizações por push sobre a frota gerenciada de WebSockets da plataforma; um job agendado no Cloud Code é a varredura de TTL, virando linhas velhas para offline e disparando o debounce; e permissões de classe mais ACLs implementam as regras de visibilidade na camada de dados, onde elas devem morar. Nada sob medida para operar — presença vira um exercício de modelagem de dados sobre infraestrutura que o backend já roda.
Perguntas frequentes
O que é um sistema de presença?
Um serviço que rastreia se cada usuário está online, ausente ou offline e transmite as mudanças, em tempo real, para quem tem permissão de vê-las. Por baixo do capô: conexões persistentes ou heartbeats periódicos escrevendo em um store rápido em memória, com as mudanças de status espalhadas por pub/sub.
Como funciona o "visto por último"?
O servidor grava um timestamp a cada heartbeat e a cada desconexão. Enquanto você está online, o status ao vivo é servido; quando você fica offline, o último timestamp gravado responde no lugar dele. É a única peça durável de um sistema no resto efêmero — o status expira, o visto por último permanece.
Qual intervalo de heartbeat usar em presença?
Trinta segundos é o padrão comum em produção, com o timeout de offline em duas a três vezes o intervalo (60 a 90 segundos). Intervalos mais curtos reduzem o atraso de detecção, mas multiplicam a carga de escrita linearmente com sua população online; o intervalo também precisa ficar abaixo dos timeouts de idle da infraestrutura, ou os proxies matam as conexões quietas antes.
Detecção por conexão ou por heartbeat — qual é melhor?
As duas, para falhas diferentes. Eventos de conexão (abrir/fechar) dão transições instantâneas, mas não pegam mortes silenciosas — um aparelho que perde energia não envia frame de fechamento e deixa uma conexão zumbi. A expiração do heartbeat limita essa defasagem ao custo de um atraso de detecção. Sistemas de produção usam eventos de conexão como caminho rápido e o TTL do heartbeat como fonte da verdade.
Como evitar que o status online fique piscando?
Faça debounce da transição para offline: na desconexão, inicie um timer de carência — comumente de 30 a 60 segundos — e cancele se o usuário reconectar, publicando "offline" só quando o timer disparar. Usuários em elevadores e túneis de metrô reconectam o tempo todo; sem o período de carência, toda lista de contatos em que eles aparecem pisca junto.
Como a presença escala para milhões de usuários?
Respeitando a matemática do fan-out: toda mudança de status precisa chegar a todo observador, então N contatos vezes M transições explode rápido. As alavancas são um store em memória com TTL para o status, inscrever-se apenas nos usuários visíveis (a lista de conversas abertas, não a agenda inteira), buscar o visto por último sob demanda em vez de empurrá-lo, e limitar a transmissão de status em grupos grandes.
Como funciona presença com vários dispositivos?
Rastreie uma conexão ou heartbeat por dispositivo e faça merge: o usuário está online enquanto qualquer dispositivo estiver, e offline só quando o último se cala. A política de merge usual é "o mais disponível vence" — online no celular ganha de ocioso no desktop — com o visto por último tirado do dispositivo mais recente.
Dá para esconder o status online?
Deveria dar — visibilidade é feature de produto, não detalhe posterior: regras como todos, só contatos ou ninguém, normalmente com reciprocidade (esconda o seu e você deixa de ver o dos outros). Dados de presença são dados comportamentais; defina quem pode observar quem no modelo de permissões, não na interface.