Introdução

Tempo real no Angular e .NET parece simples até o momento em que a equipe precisa escolher a tecnologia certa para sair do “funciona na minha máquina” e chegar em produção sem fragilidade. Em geral, a discussão começa com um pedido vago do produto: “precisamos que a tela atualize sozinha”. A partir daí aparecem quatro caminhos muito diferentes: WebSockets, Server-Sent Events (SSE), SignalR e Long Polling. Eles não são equivalentes, não resolvem o mesmo cenário com o mesmo custo e não devem ser tratados como sinônimos.

Eu escrevi este tutorial para responder a pergunta que realmente importa: quando usar cada abordagem sem superengenharia. Em alguns casos, um stream unidirecional já resolve. Em outros, a aplicação precisa de canal bidirecional, fallback automático e uma abstração que esconda a complexidade do protocolo. E, em certos ambientes corporativos, a solução mais feia na teoria vira a mais confiável na prática porque atravessa proxies, balanceadores e políticas de rede com menos atrito.

O ponto central aqui é pragmático. WebSockets oferecem comunicação bidirecional persistente. SSE entrega atualização do servidor para o navegador sobre HTTP com uma API simples. SignalR é uma abstração de alto nível para ASP.NET Core que escolhe o melhor transporte disponível e ainda oferece hubs, grupos e reconexão. Long Polling continua existindo porque compatibilidade e previsibilidade ainda ganham de elegância em ambientes onde WebSockets e SSE são bloqueados.

💡 Dica: quando o requisito é só empurrar eventos do servidor para o cliente, comece por SSE ou SignalR antes de partir para WebSockets. Em muitos produtos, a menor solução suficiente é a que sobrevive melhor ao longo do tempo.

Neste artigo eu também uso o repositório de exemplos do blog como apoio prático. O sample em .NET e Angular foi preparado para mostrar o mesmo fluxo de processamento em quatro transportes diferentes, sem esconder o que muda no frontend, no backend e na operação.

Pré-requisitos

Para acompanhar o tutorial, você vai precisar de:

  • .NET 10 SDK para executar o backend do sample
  • Node.js ou Bun para rodar o frontend Angular de demonstração
  • Conhecimento básico de ASP.NET Core, Angular e HTTP
  • Familiaridade com o conceito de endpoint, hub e requisição assíncrona

Clone o repositório de exemplos:

1
2
git clone https://github.com/lzocateli/blog-zocateli-sample.git
cd blog-zocateli-sample

Se você quiser reproduzir o cenário localmente, o backend fica em src/BlogSamples e o frontend de demonstração em frontend/search-ui-bun/. O sample foi pensado para exibir uma tarefa de processamento com evolução incremental, justamente para evidenciar a diferença entre transporte, latência e complexidade de cliente.

⚠️ Atenção: SSE depende de resposta em streaming. Em Nginx, Cloudflare e outros proxies, buffering pode atrasar ou quebrar a experiência se o backend não escrever e flushar os eventos corretamente.

Quando cada tecnologia faz sentido

Antes de entrar na implementação, eu gosto de reduzir a discussão a um critério simples: qual direção o fluxo de dados precisa seguir? Se a aplicação só precisa publicar estados, notificações ou progresso, não há motivo para pagar o custo cognitivo de uma conexão bidirecional completa. Se o cliente também precisa enviar comandos frequentes em tempo real, aí a conversa muda.

O Long Polling é o ponto de partida mais conservador. Ele funciona com qualquer infraestrutura HTTP tradicional, atravessa proxies mais restritivos e é fácil de entender: o cliente faz uma requisição, o servidor segura até haver dado novo ou até o timeout. Em troca, você paga com latência e overhead de conexões repetidas. É um compromisso útil quando compatibilidade vence elegância.

O SSE é um meio-termo muito interessante para dashboards, trilhas de auditoria, status de jobs e feeds de notificação. Ele usa uma conexão HTTP aberta e o navegador recebe eventos continuamente via EventSource. O cliente não envia mensagens pelo mesmo canal, então a solução fica naturalmente unidirecional. Isso reduz a superfície do problema, simplifica a implementação e ainda é mais compatível do que muita equipe imagina.

WebSockets entram quando a conversa precisa ser bidirecional de verdade. Chat, colaboração, jogos, telemetria interativa, edição simultânea e comandos em tempo real são cenários em que o modelo full-duplex faz sentido. O preço é conhecido: conexão persistente, atenção extra com escalabilidade, afinidade em balanceadores e maior sensibilidade a intermediários de rede.

SignalR resolve outra categoria de problema: eu quero um contrato de tempo real em ASP.NET Core sem me obrigar a escolher o transporte manualmente em cada caso. O framework negocia WebSockets, SSE e Long Polling conforme a plataforma permite. Em projetos .NET, isso reduz atrito e ainda oferece recursos de alto nível como hubs, grupos e reconexão.

Em resumo: Long Polling é a rede de segurança, SSE é o stream simples e eficiente, WebSockets é o canal persistente e bidirecional, e SignalR é a abstração que distribui a decisão para o runtime.

WebSockets na prática com .NET e Angular

WebSockets começam como HTTP e depois fazem upgrade para um canal persistente. Esse detalhe muda tudo: a conexão deixa de ser um ciclo de requisição e resposta e passa a ser um tubo aberto para ambos os lados. Isso significa menos overhead por mensagem, baixa latência e um modelo mental muito útil para aplicações que precisam reagir a interações do usuário e eventos do servidor sem reabrir conexão a cada ação.

Na prática, o ganho vem com responsabilidades extras. WebSockets exigem atenção com escalabilidade horizontal, porque a conexão permanece viva e pode depender de afinidade de sessão ou de um backplane para distribuição de mensagens. Também é comum encontrar proxy corporativo, gateway ou CDN que não trata WebSockets tão bem quanto o tráfego HTTP comum. Isso não torna a tecnologia ruim; só obriga a uma análise mais séria do ambiente.

O cenário mais forte para WebSockets em Angular e .NET é quando o cliente também envia comandos frequentes: iniciar uma tarefa, cancelar uma operação, ajustar parâmetros de execução ou manter uma sessão interativa. Em vez de abrir novas requisições para cada passo, o cliente e o servidor compartilham um canal contínuo.

📝 Exemplo: num painel de supervisão de jobs, o usuário pode disparar uma tarefa, receber progresso incremental e cancelar a operação sem sair do mesmo canal de comunicação.

SSE na prática com .NET e Angular

SSE, ou Server-Sent Events, é o oposto do glamour tecnológico e exatamente por isso ele costuma ser tão útil. O browser abre uma conexão HTTP com EventSource e fica escutando eventos nomeados enviados pelo servidor. Não há bidirecionalidade no mesmo canal; o servidor escreve, o cliente lê. Parece limitado, mas para atualização de status, notificações, progresso de processamento e feeds de eventos isso é frequentemente suficiente.

O ponto forte do SSE é a simplicidade operacional. Como ele permanece muito próximo de HTTP, costuma ser mais fácil de atravessar infraestrutura existente do que uma conexão WebSocket. A API no browser é enxuta e a reconexão pode ser tratada com comportamento nativo ou com pequena camada de código. Para aplicações Angular, isso combina bem com uma interface reativa que só precisa refletir o estado mais recente.

O cuidado principal está na entrega do stream. Se o backend escreve o evento mas o proxy segura o buffer, o usuário não vê nada até o flush ou até o buffering ser desativado. É exatamente por isso que SSE precisa ser documentado junto com a arquitetura de borda, e não apenas como um detalhe de controller.

SignalR como camada de abstração

SignalR é onde muita discussão fica confusa, então vale separar bem os papéis. SignalR não é “um protocolo concorrente” de WebSockets. Ele é uma abstração de alto nível para aplicações em ASP.NET Core que escolhe o transporte disponível e entrega uma experiência mais simples para o desenvolvedor. Se WebSockets está disponível, ótimo. Se não estiver, SignalR pode cair para SSE ou Long Polling sem quebrar o contrato do aplicativo.

Para times .NET, isso costuma ser valioso porque reduz decisões repetitivas. Em vez de cada tela decidir sozinha como falar com o backend, eu centralizo a conversa em hubs, métodos e eventos. O frontend chama um método, o hub responde com um evento, e o framework negocia o transporte no meio. Isso também ajuda quando você quer grupos, broadcast seletivo, reconexão automática e um modelo de domínio mais claro.

O trade-off é que essa camada pode esconder detalhes que a equipe deveria entender. SignalR ajuda, mas não elimina a necessidade de saber quando WebSockets, SSE ou Long Polling estão por trás da abstração. Se a aplicação precisa de previsibilidade extrema em um ambiente específico, é melhor medir o comportamento real do que assumir que o framework resolverá tudo sozinho.

Long Polling como fallback e compatibilidade máxima

Long Polling é frequentemente tratado como técnica antiga, mas isso é um erro de leitura. Ele continua sendo importante porque existe uma quantidade enorme de ambientes em que o melhor protocolo é simplesmente o que passa pelo firewall, pelo proxy e pela política de segurança sem discussão. Em vez de abrir um canal persistente, o cliente faz uma requisição e o servidor segura a resposta até haver atualização ou timeout.

O efeito prático é previsibilidade. Você pode pagar com latência maior e mais overhead de requisição, mas ganha compatibilidade quase universal. Para usuários atrás de proxies muito restritivos, ambientes corporativos com regras conservadoras e stacks legadas, Long Polling ainda é uma solução válida. É melhor ter um fallback mais lento do que não ter atualização alguma.

O problema real é tentar usar Long Polling como primeira escolha quando a aplicação claramente pede um canal mais eficiente. Se o produto depende de muitos eventos por segundo ou de interação instantânea, o custo de repetição de requisições rapidamente aparece em CPU, rede e observabilidade. Nessa situação, Long Polling deve ficar na reserva, não no volante.

Matriz de decisão para escolher a abordagem

Quando eu preciso decidir rápido, uso uma matriz simples:

Matriz de decisão entre WebSockets, SSE, SignalR e Long Polling

CenárioMelhor ponto de partidaMotivo
Somente servidor → clienteSSEMenor complexidade, stream HTTP simples
Cliente também envia comandos frequentesWebSocketsCanal bidirecional real
Quero abstração em ASP.NET Core e fallback automáticoSignalRNegociação de transporte e hubs
Infraestrutura restritiva ou legadoLong PollingMaior compatibilidade
UI de dashboard com progresso de jobsSSE ou SignalRBaixa complexidade e boa UX
Chat ou colaboração em tempo realWebSockets ou SignalRBidirecionalidade e latência baixa

Essa matriz não substitui medições, mas evita a armadilha mais comum: usar a tecnologia mais poderosa só porque ela é mais empolgante. Em produção, o menor mecanismo que cumpre o requisito é frequentemente o que dá menos trabalho para operar.

Exemplo Prático

O sample do blog foi montado para mostrar o mesmo fluxo de tarefa com quatro transportes. No backend, eu exponho um endpoint de criação, um endpoint de consulta e um stream para SSE e WebSocket. No frontend Angular, eu alterno a conexão conforme o transporte escolhido e atualizo a mesma interface com o mesmo modelo de evento.

Backend .NET

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var grupo = app.MapGroup("/api/tempo-real")
    .WithTags("Tempo real")
    .RequireCors("TempoReal");

grupo.MapPost("/tarefas", (TempoRealProcessamentoService processamentoService, TempoRealCriacaoRequest request) =>
{
    if (string.IsNullOrWhiteSpace(request.Descricao))
    {
        return Results.BadRequest(new { erro = "A descrição da tarefa é obrigatória." });
    }

    var resposta = processamentoService.CriarTarefa(request.Descricao.Trim());
    return Results.Created($"/api/tempo-real/tarefas/{resposta.TarefaId}", resposta);
});

grupo.MapGet("/tarefas/{tarefaId:guid}/stream", async (
    Guid tarefaId,
    HttpContext context,
    TempoRealProcessamentoService processamentoService,
    CancellationToken cancellationToken) =>
{
    context.Response.ContentType = "text/event-stream";
    await foreach (var proximoEstado in processamentoService.ObterFluxoAsync(tarefaId, cancellationToken))
    {
        await context.Response.WriteAsync($"data: {JsonSerializer.Serialize(proximoEstado)}\n\n", cancellationToken);
        await context.Response.Body.FlushAsync(cancellationToken);
    }
});

📂 Código Fonte: O exemplo completo do backend está disponível no repositório de exemplos do blog em BlogSamples/Messaging/TempoReal/.

Angular com SignalR

1
2
3
4
5
6
7
8
9
const connection = new signalR.HubConnectionBuilder()
  .withUrl('/hubs/tempo-real', { withCredentials: true })
  .withAutomaticReconnect()
  .build();

connection.on('estado', (estado: TempoRealEstadoResponse) => onEstado(estado));

await connection.start();
await connection.invoke('AcompanharTarefa', tarefaId);

📂 Código Fonte: O frontend de demonstração está no repositório de exemplos do blog em frontend/search-ui-bun/, onde eu adaptei a interface para alternar entre os quatro transportes.

O detalhe importante aqui não é a quantidade de linhas, mas o modelo mental. O mesmo backend produz o mesmo estado de tarefa; o que muda é como esse estado cruza a rede e como a interface decide escutar as atualizações.

Dicas e Boas Práticas

  • Comece pelo requisito de direção de dados. Se o fluxo é apenas servidor → cliente, eu prefiro SSE ou SignalR antes de pensar em WebSockets. Isso reduz complexidade, facilita a operação e evita abrir um canal mais caro do que o necessário.

  • Desative buffering quando usar SSE. Streaming sem flush de resposta vira “quase streaming”. Em produção, eu sempre valido a cadeia completa de proxy, CDN e servidor para garantir que os eventos chegam ao navegador no tempo esperado.

  • Trate SignalR como contrato, não como mágica. Ele resolve muita coisa, mas continua dependendo de transporte, reconexão e política de rede. Entender o que acontece por baixo evita surpresas quando o ambiente troca de WebSocket para Long Polling.

  • Use Long Polling como fallback consciente. Ele é útil quando a infraestrutura é restritiva, mas não é a escolha ideal para alto volume. Se a aplicação cresce, a diferença de custo em requisições e latência aparece rápido.

  • Modele o estado do evento de forma estável. O cliente não deveria precisar adivinhar o significado de cada payload. Um modelo único de evento simplifica logs, debug, reconexão e renderização na UI.

  • Observe o ambiente de borda desde o início. Proxies, balanceadores, políticas de segurança e timeouts são parte da arquitetura de tempo real, não detalhes operacionais. Ignorar isso gera um sistema que funciona no dev e falha fora dele.

Resumo Objetivo

  • WebSockets — oferecem comunicação bidirecional persistente sobre uma conexão única, o que reduz overhead por mensagem e favorece cenários interativos como chat e colaboração.
  • SSE — entregam eventos do servidor para o navegador via text/event-stream, com API nativa no browser e simplicidade operacional para dashboards e notificações.
  • SignalR — é uma abstração de alto nível do ASP.NET Core que negocia WebSockets, SSE ou Long Polling e expõe hubs, grupos e reconexão.
  • Long Polling — mantém a compatibilidade máxima com infraestrutura HTTP tradicional, mas paga com mais latência e mais requisições ao servidor.
  • Angular + .NET — funcionam muito bem para aplicações de tempo real quando o modelo de evento é estável e o transporte é escolhido pelo requisito, não pela moda.
  • Infraestrutura de borda — pode ser o fator decisivo entre uma solução elegante e uma solução operacionalmente confiável; buffering e proxies influenciam SSE e WebSockets diretamente.

Leia Também

Referências