Dashboards em Tempo Real com Django, HTMX e Redis Streams: Guia Completo

Published on: 2025-12-11
Post image
pt dashboards-em-tempo-real django htmx redis-streams server-sent-events atualizacoes-ao-vivo desenvolvimento-web python-backend arquitetura-reativa streaming-de-dados

Dashboards em tempo real aparecem em muitos contextos diferentes, desde plataformas de análise de dados até painéis de monitoramento de sistemas. A sensação de acompanhar números mudando ao vivo costuma ser associada a pilhas de tecnologia complexas e cheias de JavaScript. Ao longo deste artigo, a proposta é mostrar um caminho mais simples e direto usando apenas ferramentas bem conhecidas no mundo Python.

A construção do painel descrito aqui usa três peças principais: Django para o backend, HTMX para atualizar o HTML de forma dinâmica no navegador e Redis Streams como canal de distribuição de eventos em tempo real. A narrativa segue uma experiência prática, em primeira pessoa, detalhando desde a ideia inicial até os cuidados de produção, passando por exemplos completos de código.

Motivação para usar Django, HTMX e Redis Streams

A escolha desse conjunto de tecnologias nasceu de uma necessidade bem simples: atualizar dados em tempo real sem mergulhar em *frameworks* complexos de frontend. Django já fazia parte do ambiente por ser um framework web robusto, estável e com ótimo ecossistema. HTMX entrou na história para permitir que partes da página fossem atualizadas trocando apenas HTML pelo HTTP, sem lidar com estados complexos no lado do navegador. Redis Streams apareceu como um mecanismo de filas de mensagens, ideal para distribuir eventos de forma eficiente.

Essa combinação trouxe uma vantagem importante: quase todo o código ficou concentrado em Python e HTML, deixando o JavaScript em um papel mínimo. Não houve necessidade de ferramentas como npm, bundlers ou processos de build para o frontend. O fluxo mental ficou muito mais linear: Django gera HTML, HTMX insere esse HTML na página e Redis Streams conecta produtores e consumidores de eventos. A sensação foi de montar um sistema reativo, porém com peças conhecidas e pouco atrito de configuração.

Diferença entre polling e modelo de push em tempo real

Antes de estruturar o código, ficou claro que seria preciso decidir entre dois modelos de atualização: polling (modelo de busca) e push (modelo de envio). Polling significa que o navegador faz requisições periódicas perguntando se há novidades; esse padrão é simples, mas costuma gerar muitas requisições inúteis. Em um cenário com muitos usuários, cada um perguntando a cada poucos segundos, o servidor acaba gastando tempo respondendo com “nada novo” o tempo todo. Além disso, a latência fica presa ao intervalo do polling, porque uma atualização gerada agora só vai aparecer quando o próximo ciclo chegar.

No modelo de push, o servidor abre uma conexão contínua com o navegador e envia dados apenas quando algo de fato muda. É aí que entra o conceito de Server-Sent Events, conhecido pela sigla SSE, em que o navegador estabelece uma conexão unidirecional e recebe mensagens sempre que o servidor emite novos eventos. Esse padrão trouxe três ganhos bem nítidos: uso mais eficiente de recursos, atualizações praticamente imediatas e um modelo mental simples, já que o fluxo é sempre servidor → cliente. A partir desse ponto, a escolha foi seguir com SSE, usando a extensão de SSE do HTMX para simplificar ainda mais o lado do navegador.

Visão geral da arquitetura do dashboard em tempo real

A arquitetura acabou ficando organizada em três camadas principais, cada uma com uma responsabilidade bem clara. A primeira camada é formada pelos produtores de eventos, que coletam ou geram dados e os publicam em um stream do Redis, como métricas de CPU, memória ou qualquer outro tipo de métrica de negócio. A segunda camada é o próprio Redis Streams, que atua como um intermediário confiável, armazenando os eventos e permitindo que consumidores leiam esses dados em ordem.

A terceira camada é composta pelas views Django que consomem os eventos do Redis e transformam essas informações em fragmentos de HTML. Esses fragmentos são enviados ao navegador por meio de uma conexão SSE. Do lado do navegador, o HTMX recebe o HTML pronto e substitui o conteúdo de elementos específicos da página, mantendo o layout geral intacto. Essa separação em produtor, broker e consumidor visual trouxe uma sensação de clareza: cada parte da aplicação sabe exatamente o que faz, sem sobreposição de responsabilidades.

Configuração inicial do Django e integração com o Redis

O primeiro passo prático foi garantir que Django e Redis conversassem bem entre si. A instalação necessária envolveu Django, o cliente oficial de Redis para Python e o hiredis para otimizar o desempenho de parsing de respostas. Com os pacotes prontos, o próximo passo foi definir, em um único ponto de configuração, como o Redis seria acessado pela aplicação. Esse tipo de centralização facilita ajustes futuros, como mudar de host, porta ou banco.

O trecho a seguir mostra a configuração básica usada no arquivo de configuração do Django e uma pequena classe utilitária para lidar com operações de stream:

pip install django redis hiredis
# settings.py
REDIS_CONFIG = {
    'host': 'localhost',
    'port': 6379,
    'db': 0,
    'decode_responses': True  # retorna strings em vez de bytes
}
# utils/redis_client.py
import redis
from django.conf import settings

class RedisStreamClient:
    def __init__(self):
        self.client = redis.Redis(**settings.REDIS_CONFIG)

    def publicar_evento(self, nome_stream, dados):
        """Publica um evento em um stream do Redis"""
        return self.client.xadd(nome_stream, dados)

    def consumir_eventos(self, nome_stream, ultimo_id='$', bloquear=5000):
        """
        Consome eventos de um stream do Redis.
        bloquear=5000 significa esperar até 5 segundos por novos eventos.
        """
        streams = {nome_stream: ultimo_id}
        resultados = self.client.xread(streams, block=bloquear, count=10)

        if resultados:
            stream, mensagens = resultados[0]
            return mensagens
        return []

redis_cliente = RedisStreamClient()

Nessa configuração, o método publicar_evento escreve novos itens no stream, enquanto consumir_eventos faz uma leitura bloqueante, aguardando até que novos dados apareçam ou que o tempo de espera termine. O parâmetro ultimo_id permite controlar a posição de leitura no stream, garantindo que cada evento seja processado apenas uma vez dentro do fluxo de SSE.

Publicação de métricas em tempo real como produtora de eventos

Com o Redis integrado, o próximo passo foi definir uma fonte concreta de dados para alimentar o dashboard. Uma escolha bastante prática para essa demonstração foi usar o pacote psutil para coletar métricas do sistema, como uso de CPU, memória e disco. Essas métricas são então agrupadas em um dicionário, serializadas em JSON e enviadas para um stream específico no Redis, dedicado ao painel de métricas.

O serviço abaixo resume esse processo de coleta e publicação:

pip install psutil
# services/servico_metricas.py
import psutil
import json
from datetime import datetime
from utils.redis_client import redis_cliente

class ServicoMetricas:
    NOME_STREAM = 'dashboard:metricas'

    @classmethod
    def publicar_metricas_sistema(cls):
        """Coleta e publica métricas do sistema"""
        metricas = {
            'cpu_percentual': psutil.cpu_percent(interval=1),
            'memoria_percentual': psutil.virtual_memory().percent,
            'disco_percentual': psutil.disk_usage('/').percent,
            'timestamp': datetime.now().isoformat()
        }

        redis_cliente.publicar_evento(
            cls.NOME_STREAM,
            {'dados': json.dumps(metricas)}
        )
        return metricas

Para manter o fluxo constante de informações, foi criado um comando de gerenciamento do Django que chama esse serviço em um laço infinito, com uma pequena pausa entre cada publicação. Isso permite rodar um processo separado apenas para alimentar o stream com novas métricas:

# management/commands/publicar_metricas.py
from django.core.management.base import BaseCommand
import time
from services.servico_metricas import ServicoMetricas

class Command(BaseCommand):
    help = 'Publica continuamente métricas do sistema'

    def handle(self, *args, **options):
        while True:
            ServicoMetricas.publicar_metricas_sistema()
            time.sleep(2)  # publica a cada 2 segundos

Essa abordagem de comando de gerenciamento manteve a lógica de coleta isolada e fácil de controlar, além de permitir que múltiplos produtores diferentes fossem adicionados no futuro, cada um publicando em streams específicos de acordo com o tipo de dado monitorado.

Construção da view de streaming com Server-Sent Events

A parte mais interessante surgiu na hora de transformar esses eventos do Redis em uma resposta contínua para o navegador. Django oferece o StreamingHttpResponse, que possibilita enviar dados ao cliente em partes, sem encerrar a conexão. Em conjunto com o formato de mensagens SSE, ficou possível empacotar cada fragmento de HTML gerado e enviá-lo linha a linha. O formato básico de SSE consiste em linhas que começam com “data:” seguidas do conteúdo e terminadas por uma linha em branco.

O código abaixo ilustra uma view capaz de consumir o stream do Redis e empurrar atualizações de métricas em tempo real para o navegador, além de uma função auxiliar que monta o HTML da “cartão” de métricas:

# views.py
from django.http import StreamingHttpResponse
from django.views.decorators.http import require_http_methods
from utils.redis_client import redis_cliente
import json

@require_http_methods(["GET"])
def fluxo_metricas(request):
    """
    Faz stream de atualizações de métricas para o cliente usando SSE.
    """
    def gerador_eventos():
        nome_stream = 'dashboard:metricas'
        ultimo_id = '$'  # começa a partir do evento mais recente

        while True:
            mensagens = redis_cliente.consumir_eventos(
                nome_stream,
                ultimo_id=ultimo_id,
                bloquear=5000  # espera até 5 segundos por novos eventos
            )

            for mensagem_id, dados in mensagens:
                ultimo_id = mensagem_id
                # como decode_responses=True, os campos já vêm como strings
                metricas_dados = json.loads(dados['dados'])

                # renderiza fragmento HTML
                html = renderizar_cartao_metricas(metricas_dados)

                # formata como SSE
                yield f"data: {html}\n\n"

            # envia um comentário como keep-alive quando não houver mensagens
            if not mensagens:
                yield f": keepalive\n\n"

    resposta = StreamingHttpResponse(
        gerador_eventos(),
        content_type='text/event-stream'
    )
    resposta['Cache-Control'] = 'no-cache'
    resposta['X-Accel-Buffering'] = 'no'  # desativa buffering em servidores reversos
    return resposta


def renderizar_cartao_metricas(metricas):
    """Gera HTML para exibir métricas no dashboard."""
    return f"""
    <div class="metrics-card">
        <div class="metric">
            <span class="label">CPU</span>
            <span class="value">{metricas['cpu_percentual']:.1f}%</span>
        </div>
        <div class="metric">
            <span class="label">Memória</span>
            <span class="value">{metricas['memoria_percentual']:.1f}%</span>
        </div>
        <div class="metric">
            <span class="label">Disco</span>
            <span class="value">{metricas['disco_percentual']:.1f}%</span>
        </div>
        <div class="timestamp">
            Atualizado: {metricas['timestamp']}</div>
    </div>
    """

Nesse fluxo, a função gerador_eventos fica em um laço contínuo, buscando novos eventos no Redis e, à medida que cada um chega, gerando um pequeno pedaço de HTML já pronto para inserção via HTMX. O uso de comentários SSE como “keepalive” ajuda a manter a conexão saudável, mesmo quando não há dados novos por alguns segundos.

Estrutura da página HTML e uso do HTMX com SSE

Do lado do frontend, a proposta foi manter tudo o mais simples possível. Com HTMX, bastou adicionar alguns atributos especiais a um contêiner HTML para que uma conexão SSE fosse aberta automaticamente e os fragmentos de HTML recebidos fossem injetados no DOM. A extensão de SSE do HTMX entende o formato de mensagens SSE e dispara eventos que podem ser usados para atualizar elementos de forma declarativa.

O exemplo abaixo mostra um esqueleto de template Django com o HTMX carregado e um bloco dedicado ao dashboard de métricas do sistema:

<!DOCTYPE html>
<html>
<head>
    <title>Dashboard em Tempo Real</title>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/sse.js"></script>
</head>
<body>
    <div class="metrics-container">
        <h1>Métricas do Sistema</h1>

        <div hx-ext="sse"
             sse-connect="/dashboard/fluxo-metricas/"
             sse-swap="message">

            <div id="metrics-display">
                <div class="metrics-card">
                    <p>Conectando ao stream em tempo real...</p>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

O atributo hx-ext="sse" ativa a extensão de SSE do HTMX, enquanto sse-connect indica qual endpoint deve ser usado para abrir a conexão de eventos. Já sse-swap="message" instrui o HTMX a substituir o conteúdo do elemento sempre que uma nova mensagem de tipo “message” chegar. Na prática, cada fragmento HTML enviado pelo Django via SSE entra no lugar do conteúdo atual, mantendo o painel sempre atualizado.

Configuração das URLs e da view principal do dashboard

Para amarrar tudo isso, as rotas do Django precisaram refletir tanto a página de dashboard quanto o endpoint de SSE. A estrutura pensada foi simples: uma URL para exibir o HTML estático inicial do painel e outra URL para o fluxo contínuo das métricas. Isso permite que a página carregue rapidamente com algum conteúdo inicial e, em seguida, estabeleça a conexão SSE em segundo plano através do HTMX.

O código a seguir mostra um exemplo de configuração de URLs e de uma view básica para renderizar o template do dashboard:

# views.py (continuação)
from django.shortcuts import render

def dashboard_metricas(request):
    """Renderiza a página principal do dashboard de métricas."""
    return render(request, 'dashboard.html')
# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('dashboard/', views.dashboard_metricas, name='dashboard_metricas'),
    path('dashboard/fluxo-metricas/', views.fluxo_metricas, name='fluxo_metricas'),
]

Com esse arranjo, o fluxo de acesso fica bem natural: primeiro ocorre uma requisição HTTP comum para “/dashboard/”, que entrega o HTML inicial, e em seguida o HTMX abre silenciosamente uma conexão SSE com “/dashboard/fluxo-metricas/”. A partir daí, todo o restante do trabalho de atualização passa a ser conduzido por essa conexão contínua.

Trabalhando com múltiplos streams de dados no mesmo dashboard

Um cenário muito comum em dashboards reais é a necessidade de exibir vários tipos de dados em tempo real ao mesmo tempo. Em vez de um único stream, surgem múltiplos streams no Redis, cada um representando um conjunto diferente de eventos, como métricas do sistema, atividade de usuários e vendas recentes. A abordagem escolhida usa vários contêineres HTMX, cada um com sua própria conexão SSE independente.

O fragmento abaixo ilustra uma estrutura de página com três widgets, cada um conectado a um stream diferente:

<div class="dashboard-grid">
    <div hx-ext="sse"
         sse-connect="/streams/metricas-sistema/"
         sse-swap="message"
         class="widget">
        <div id="metricas-sistema">Carregando métricas do sistema...</div>
    </div>

    <div hx-ext="sse"
         sse-connect="/streams/atividade-usuarios/"
         sse-swap="message"
         class="widget">
        <div id="atividade-usuarios">Carregando atividade de usuários...</div>
    </div>

    <div hx-ext="sse"
         sse-connect="/streams/vendas/"
         sse-swap="message"
         class="widget">
        <div id="vendas">Carregando dados de vendas...</div>
    </div>
</div>

Nessa configuração, cada widget controla sua própria área do DOM e reage aos dados que chegam exclusivamente pelo seu endpoint SSE. Do ponto de vista do servidor, esse padrão se traduz em múltiplas views de streaming semelhantes à de métricas, cada uma consumindo um stream distinto no Redis. Essa modularidade ajudou muito na manutenção e na expansão do dashboard, já que novos widgets podem ser adicionados sem interferir nos fluxos existentes.

Tratamento de erros, reconexão e feedback visual

Em qualquer solução de tempo real, falhas de conexão e quedas temporárias são parte do jogo. Uma das vantagens da combinação HTMX + SSE é a capacidade de lidar com reconexão automática, reduzindo bastante a quantidade de JavaScript personalizado necessário. Mesmo assim, tornou-se útil adicionar alguns ganchos para fornecer feedback visual sobre o status da conexão, como um indicador de “conectado” ou “desconectado”.

O exemplo abaixo mostra como alguns eventos do HTMX podem ser usados para acionar funções JavaScript simples sempre que uma mensagem for recebida ou quando ocorrer um erro de SSE:

<div id="indicador-status" class="status-indicator">Conectando...</div>

<div hx-ext="sse"
     sse-connect="/dashboard/fluxo-metricas/"
     sse-swap="message"
     hx-on::after-settle="tratarAtualizacaoStream()"
     hx-on::sse-error="tratarErroStream()">

    <div id="metrics-display">Carregando...</div>
</div>

<script>
function tratarAtualizacaoStream() {
    // atualização recebida com sucesso
    const indicador = document.getElementById('indicador-status');
    indicador.textContent = 'Conectado';
    indicador.classList.remove('desconectado');
    indicador.classList.add('conectado');
}

function tratarErroStream() {
    // erro na conexão, HTMX tentará reconectar automaticamente
    const indicador = document.getElementById('indicador-status');
    indicador.textContent = 'Reconectando...';
    indicador.classList.remove('conectado');
    indicador.classList.add('desconectado');
    console.error('Erro na conexão do stream - HTMX fará nova tentativa.');
}
</script>

Com esse pequeno complemento, o comportamento de reconexão padrão do HTMX permaneceu intacto, mas o usuário passou a ter uma indicação clara do que está acontecendo em caso de instabilidade de rede. Além disso, logs de erros no console facilitaram a depuração durante o desenvolvimento e nos primeiros testes em ambientes de homologação.

Cuidados de produção com servidor web e workers assíncronos

Ao levar esse tipo de solução para produção, surgiram algumas preocupações importantes em relação à infraestrutura. Conexões SSE podem permanecer abertas durante longos períodos, às vezes por horas, o que exige que o servidor web e o application server sejam capazes de lidar bem com conexões de longa duração. Em cenários envolvendo Nginx e Gunicorn, por exemplo, foi necessário ajustar opções específicas para desativar buffering e permitir HTTP 1.1 corretamente.

O trecho a seguir destaca uma configuração típica em Nginx e um comando de execução do Gunicorn usando workers assíncronos baseados em gevent:

location /dashboard/fluxo-metricas/ {
    proxy_pass http://django;
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
}
gunicorn --workers 4 --worker-class gevent --timeout 300 meu_projeto.wsgi:application

O uso de um worker-class assíncrono como gevent ou eventlet ajudou a manter muitas conexões abertas sem saturar o número de processos. Além disso, foi importante monitorar limites de conexão do próprio servidor reverse proxy, para evitar encerramentos prematuros de streams por causa de timeouts ou políticas de keep-alive muito agressivas.

Limpeza de streams do Redis e considerações de escalabilidade

Outra questão que apareceu rapidamente foi o crescimento contínuo dos streams do Redis. Sem algum tipo de limpeza, o stream de métricas poderia acumular milhões de eventos ao longo do tempo, consumindo memória desnecessariamente. A funcionalidade de xtrim do Redis oferece uma forma simples de limitar o tamanho máximo do stream, preservando apenas as mensagens mais recentes em uma janela de interesse.

O exemplo a seguir mostra uma chamada típica de limpeza após a publicação de métricas, mantendo apenas os últimos mil eventos:

# trecho dentro de ServicoMetricas.publicar_metricas_sistema
redis_cliente.client.xtrim(
    'dashboard:metricas',
    maxlen=1000,
    approximate=True  # permite aparar de forma aproximada para melhor desempenho
)

Em termos de escalabilidade, o fato de o Redis Streams ser um serviço independente permitiu que múltiplas instâncias do Django fossem executadas em paralelo, todas consumindo do mesmo fluxo sem conflitos. Em cenários ainda mais complexos, há espaço para uso de grupos de consumidores do Redis, o que traz garantias adicionais de entrega e controle de paralelismo. Mesmo sem ir tão longe, essa arquitetura já mostrou grande capacidade de expansão horizontal, apenas replicando instâncias de aplicação atrás de um balanceador de carga.

Aplicações práticas e benefícios percebidos na experiência

Ao longo do uso desse padrão, alguns tipos de aplicação mostraram encaixe especialmente bom. Painéis de análise em tempo real, exibindo comportamento de usuários ou taxas de conversão, aproveitaram bastante a atualização constante sem recarregar a página. Sistemas de monitoramento de infraestrutura, acompanhando métricas de servidores, também se beneficiaram da simplicidade de adicionar novos tipos de métricas como novos streams e widgets. Além disso, casos como acompanhamento de pedidos ou notificações ao vivo ganharam versões mais leves e diretas sem a necessidade de WebSockets.

Do ponto de vista da experiência de desenvolvimento, a principal sensação foi de equilíbrio entre simplicidade e poder. O modelo SSE com HTMX ficou mais simples que uma pilha completa com WebSockets e bibliotecas robustas de frontend, ao mesmo tempo em que se mostrou mais eficiente e elegante do que o polling tradicional. As peças se encaixaram de forma natural: Django continuou cuidando bem do backend, Redis Streams assumiu o papel de canal de eventos em tempo real e HTMX permitiu manter o controle da interface quase todo em HTML, preservando a clareza do código e facilitando a manutenção no longo prazo.