A capacidade de entregar informações instantaneamente define a qualidade de uma aplicação moderna. Seja um chat, um painel de monitoramento ou uma plataforma colaborativa, usuários esperam ver mudanças acontecendo — não descobri-las na próxima vez que recarregarem a página.
Este artigo apresenta uma abordagem prática para projetar e implementar um sistema de notificações em tempo real, com exemplos em JavaScript, Python e Rust. Ao final, você terá clareza sobre os princípios fundamentais, os padrões recomendados e as armadilhas mais comuns.
Push vs. Pull: a diferença que importa
Um Push Notification System é uma arquitetura em que o servidor envia mensagens diretamente aos clientes conectados, sem que esses clientes precisem realizar requisições periódicas (polling).
"Em vez de o cliente perguntar 'tem novidade?' a cada segundo, o servidor avisa quando há algo novo."
As principais tecnologias para estabelecer esse canal são:
| Tecnologia | Comunicação | Melhor uso | Limitação |
|---|---|---|---|
| WebSockets | Full-duplex | Apps bidirecionais | Conexão persistente por cliente |
| Socket.IO | Full-duplex | Compatibilidade ampla | Overhead de abstração |
| SSE | Servidor → cliente | Feeds, dashboards | Unidirecional |
| Long Polling | Requisição/resposta | Fallback legado | Alta latência |
O problema com polling
Polling — fazer requisições periódicas ao servidor — é simples de implementar, mas tem custos reais:
- Latência artificial: o usuário só vê a atualização no próximo ciclo de polling, que pode ser segundos depois do evento.
- Carga desnecessária: requisições acontecem mesmo quando não há nada novo.
- Escalabilidade limitada: 10.000 clientes fazendo polling a cada 2 segundos geram 300.000 requisições por minuto, a maioria respondendo "nenhuma novidade".
WebSockets resolvem esses três problemas de uma só vez.
Como o sistema se organiza
Um sistema bem estruturado passa por cinco etapas:
- Estabelecimento da conexão: o cliente abre uma conexão persistente com o servidor via WebSocket.
- Autenticação: o servidor valida a identidade do cliente e associa a conexão a um usuário ou sessão.
- Agrupamento em canais: conexões são organizadas por contexto (usuário, sala, tópico) para envios seletivos.
- Publicação de eventos: quando algo relevante ocorre, o servidor emite notificações para os clientes do canal correspondente.
- Reconexão automática: em caso de queda, o cliente tenta reconectar sem perder o contexto.
Atenção: armazene o mapeamento de
socketId → userIdem uma estrutura centralizada (Redis em ambientes distribuídos). Depender apenas da memória do processo impede escalabilidade horizontal.
Implementação
JavaScript — servidor com Node.js e Socket.IO
Socket.IO é a escolha mais comum no ecossistema Node.js: abstrai WebSockets, oferece fallbacks automáticos e tem suporte nativo a salas.
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const server = createServer(app);
const io = new Server(server, {
cors: { origin: '*' },
pingTimeout: 10_000,
pingInterval: 5_000,
});
// userId → Set de socketIds
const userSockets = new Map();
// Middleware de autenticação
io.use((socket, next) => {
const token = socket.handshake.auth?.token;
if (!token) return next(new Error('Não autorizado'));
socket.userId = verifyToken(token); // implemente conforme seu sistema
next();
});
io.on('connection', (socket) => {
const { userId } = socket;
// Registra conexão
if (!userSockets.has(userId)) userSockets.set(userId, new Set());
userSockets.get(userId).add(socket.id);
socket.join(`user:${userId}`);
// Entrega notificações pendentes ao reconectar
socket.emit('pending', getPendingNotifications(userId));
socket.on('disconnect', () => {
const sockets = userSockets.get(userId);
sockets?.delete(socket.id);
if (sockets?.size === 0) userSockets.delete(userId);
});
});
// Notifica um usuário específico a partir de qualquer lugar da aplicação
export function notifyUser(userId, event, payload) {
io.to(`user:${userId}`).emit(event, payload);
}
server.listen(3000, () => console.log('Servidor rodando na porta 3000'));
JavaScript — cliente
import { io } from 'socket.io-client';
const socket = io('https://seu-servidor.com', {
auth: { token: localStorage.getItem('token') },
reconnectionAttempts: 5,
reconnectionDelay: 2000,
});
socket.on('connect', () => console.log('Conectado:', socket.id));
socket.on('pending', (notifications) => {
notifications.forEach(render);
});
socket.on('nova-mensagem', (data) => render(data));
socket.on('connect_error', (err) => {
console.error('Falha na conexão:', err.message);
});
function render(notification) {
// renderiza a notificação na UI
}
Python — servidor com FastAPI e WebSockets
Para backends Python, FastAPI oferece suporte nativo a WebSockets sem dependências adicionais.
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from collections import defaultdict
from typing import DefaultDict, Set
import json
app = FastAPI()
# userId → conjunto de conexões ativas
connections: DefaultDict[str, Set[WebSocket]] = defaultdict(set)
def get_user_id(token: str) -> str:
# Implemente sua lógica de autenticação
return verify_token(token)
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket, token: str):
await websocket.accept()
try:
user_id = get_user_id(token)
except Exception:
await websocket.close(code=4001)
return
connections[user_id].add(websocket)
# Entrega notificações pendentes
pending = get_pending_notifications(user_id)
if pending:
await websocket.send_text(json.dumps({"type": "pending", "data": pending}))
try:
while True:
# Mantém a conexão viva; mensagens do cliente podem ser processadas aqui
await websocket.receive_text()
except WebSocketDisconnect:
connections[user_id].discard(websocket)
if not connections[user_id]:
del connections[user_id]
async def notify_user(user_id: str, event: str, payload: dict):
"""Envia uma notificação a todas as conexões ativas de um usuário."""
message = json.dumps({"type": event, "data": payload})
dead = set()
for ws in connections.get(user_id, set()):
try:
await ws.send_text(message)
except Exception:
dead.add(ws)
connections[user_id] -= dead
Rust — servidor com Axum e Tokio
Para sistemas onde performance e consumo de memória são críticos, Rust com Axum oferece WebSockets com overhead mínimo.
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
extract::State,
response::Response,
routing::get,
Router,
};
use dashmap::DashMap;
use std::sync::Arc;
use tokio::sync::broadcast;
#[derive(Clone)]
struct AppState {
// userId → canal de broadcast
channels: Arc<DashMap<String, broadcast::Sender<String>>>,
}
async fn ws_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
// token viria via query param ou header
) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
async fn handle_socket(mut socket: WebSocket, state: AppState) {
// Em produção, extraia e valide o token aqui
let user_id = "user_123".to_string();
let sender = state
.channels
.entry(user_id.clone())
.or_insert_with(|| {
let (tx, _) = broadcast::channel(32);
tx
})
.clone();
let mut receiver = sender.subscribe();
loop {
tokio::select! {
// Mensagem chegando do servidor para o cliente
Ok(msg) = receiver.recv() => {
if socket.send(Message::Text(msg)).await.is_err() {
break; // cliente desconectou
}
}
// Mensagem chegando do cliente (ping, ack, etc.)
Some(Ok(msg)) = socket.recv() => {
if let Message::Close(_) = msg {
break;
}
}
else => break,
}
}
}
// Notifica um usuário a partir de qualquer parte da aplicação
fn notify_user(state: &AppState, user_id: &str, payload: &str) {
if let Some(sender) = state.channels.get(user_id) {
let _ = sender.send(payload.to_string());
}
}
#[tokio::main]
async fn main() {
let state = AppState {
channels: Arc::new(DashMap::new()),
};
let app = Router::new()
.route("/ws", get(ws_handler))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Dependências (
Cargo.toml):axum,tokio(features:full),dashmap
O que faz diferença no dia a dia
Canais em vez de broadcast global: emitir eventos para todos os clientes conectados é o caminho mais rápido para desperdiçar banda e CPU. Agrupe conexões por contexto — usuário, organização, sala — e envie apenas para quem precisa receber.
Notificações persistentes: nem todo usuário estará online quando um evento ocorrer. Persista notificações não entregues e envie-as ao reconectar, como demonstrado nos exemplos com pending. Sem isso, mensagens simplesmente somem.
Heartbeats calibrados: pingTimeout e pingInterval detectam conexões zumbis. Valores muito baixos causam reconexões desnecessárias em redes instáveis; muito altos atrasam a detecção de clientes mortos. Ajuste conforme o ambiente.
Logging de entrega: registre quando uma notificação é enviada, para qual usuário e se chegou. Sem isso, depurar comportamentos inconsistentes entre ambientes vira adivinhação.
O que costuma dar errado
Broadcast global: além do desperdício de recursos, expõe dados de um usuário para outro. Sempre filtre o destino.
Estado só em memória: em múltiplos processos ou containers, conexões do mesmo usuário podem estar em instâncias diferentes. Use Redis com o adapter correspondente — Socket.IO Redis Adapter no Node, pub/sub nativo no Python e Rust — para que eventos cruzem instâncias.
Tokens expirados sem desconexão: sessões inativas ou tokens vencidos que não são desconectados acumulam conexões fantasma. O servidor continua mantendo estado para clientes que, na prática, já foram embora.
Reconexão sem backoff: clientes que reconectam imediatamente após uma falha criam picos de carga exatamente quando o servidor está mais vulnerável. Exponential backoff com jitter distribui as tentativas no tempo.
Quando o sistema precisa crescer
Com múltiplas instâncias, dois componentes se tornam indispensáveis:
- Pub/Sub centralizado (Redis, NATS): sincroniza eventos entre processos distintos. Um evento publicado em uma instância chega a clientes conectados em outra.
- Load balancer com sticky sessions ou transporte WebSocket direto: garante que a conexão do cliente seja roteada consistentemente.
// Socket.IO + Redis Adapter
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'ioredis';
const pub = createClient({ host: 'redis', port: 6379 });
const sub = pub.duplicate();
io.adapter(createAdapter(pub, sub));
Para ir além
Notificações em tempo real são mais do que WebSockets. O WebSocket é só o transporte — a complexidade real está em autenticação, persistência de mensagens não entregues, estado distribuído e reconexão sem perda de contexto.
Os padrões deste artigo cobrem bem a maioria dos casos. Quando o sistema crescer além deles, o próximo passo natural são arquiteturas orientadas a eventos com filas de mensagens (RabbitMQ, Kafka) — onde o WebSocket vira apenas a camada de entrega final, e a lógica de roteamento fica em outro lugar.
Referências
- Socket.IO. Client Initialization. https://socket.io/docs/v4/client-initialization/
- Socket.IO. Redis Adapter. https://socket.io/docs/v4/redis-adapter/
- FastAPI. WebSockets. https://fastapi.tiangolo.com/advanced/websockets/
- Axum. WebSocket example. https://github.com/tokio-rs/axum/tree/main/examples/websockets
- MDN Web Docs. WebSockets API. https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API