Przejdź do treści głównej

Building Real-Time Applications with SignalR and Azure

Master SignalR for building production-ready real-time applications. From WebSockets fundamentals to scaling with Redis backplane and Azure SignalR Service, learn everything you need to build chat apps, live dashboards, and collaborative tools that handle millions of concurrent connections.

Author: Michał Wojciechowski··20 min read
Real-time communication infrastructure and cloud architecture

What is SignalR and Why Use It in 2025?

SignalR is a real-time communication library for ASP.NET that makes it incredibly simple to add real-time web functionality to applications. Built by Microsoft and integrated with Azure, it handles the complexity of managing persistent connections, choosing the best transport protocol, and scaling across multiple servers.

Think of SignalR as the bridge between your server and clients that keeps both sides in sync instantly. When data changes on the server, clients get notified immediately without polling or refreshing. It's the technology behind chat applications, live sports scores, real-time dashboards, collaborative editing tools, and multiplayer games.

Why SignalR in 2025:

  • Automatic transport negotiation – SignalR picks the best protocol (WebSockets, SSE, Long Polling) based on client capabilities
  • Built-in scaling – Azure SignalR Service handles millions of connections with zero server management
  • Cross-platform clients – JavaScript, .NET, Java, Swift clients work seamlessly with any frontend framework
  • Integrated authentication – Works with ASP.NET Core Identity, JWT, Azure AD out of the box
  • Production-proven – Used by Microsoft Teams, Bing, Xbox Live, and thousands of enterprise applications

According to Microsoft's SignalR documentation, the library handles over 100,000+ concurrent connections per server with proper configuration. With Azure SignalR Service, this scales to millions. Whether you're building multi-tenant SaaS applications with real-time features or planning to deploy to Azure AKS, SignalR on the .NET platform should be your first choice for real-time features.

How SignalR Works - Transports and Protocols

SignalR supports three transport protocols and automatically negotiates the best one during connection establishment. Understanding these transports helps you optimize performance and troubleshoot connection issues.

WebSockets (Preferred)

WebSockets provide full-duplex bidirectional communication over a single TCP connection. Lowest latency, minimal overhead, and perfect for real-time apps.

  • Latency: 1-10ms typical roundtrip time
  • Overhead: 2-6 bytes per frame (minimal)
  • Requirements: HTTP/1.1 upgrade, no proxy restrictions
  • Best for: Chat, multiplayer games, live collaboration, trading platforms

Server-Sent Events (SSE)

Server-Sent Events provide unidirectional streaming from server to client over HTTP. Simpler than WebSockets but server-to-client only.

  • Latency: 10-50ms typical (HTTP overhead)
  • Overhead: HTTP headers per message (higher than WebSockets)
  • Requirements: HTTP/1.1, works through most proxies
  • Best for: Live feeds, notifications, stock tickers, social media updates

Long Polling (Fallback)

Long Polling is a request-response pattern where clients repeatedly poll the server. Highest latency but maximum compatibility.

  • Latency: 100-1000ms (depends on poll interval)
  • Overhead: Full HTTP headers per request (highest overhead)
  • Requirements: Basic HTTP, works everywhere
  • Best for: Legacy browsers, restrictive corporate networks

Transport Negotiation Process

When a client connects, SignalR automatically negotiates the best transport:

  1. Client sends HTTP POST to /negotiate endpoint
  2. Server responds with available transports and connection token
  3. Client tries WebSockets first (if supported by both client and server)
  4. Falls back to Server-Sent Events if WebSockets unavailable
  5. Falls back to Long Polling as last resort

You can skip negotiation and force a specific transport, but automatic negotiation is recommended for maximum compatibility.

SignalR Server Setup - ASP.NET Core Implementation

Setting up SignalR on the server is straightforward. You create Hub classes that define methods clients can call, then configure SignalR middleware in your ASP.NET Core application.

Step 1: Install SignalR Package

# Install SignalR package (included in ASP.NET Core)
dotnet add package Microsoft.AspNetCore.SignalR

# For Redis backplane (optional, for scaling)
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis

Step 2: Create a Hub

Hubs are the core of SignalR. They define methods that clients can call and provide APIs to send messages to clients.

// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Authorization;

[Authorize] // Require authentication
public class ChatHub : Hub
{
    // Client calls this method to send a message
    public async Task SendMessage(string user, string message)
    {
        // Broadcast to all connected clients
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Send to specific user
    public async Task SendPrivateMessage(string toUserId, string message)
    {
        await Clients.User(toUserId).SendAsync("ReceivePrivateMessage",
            Context.User?.Identity?.Name, message);
    }

    // Send to specific group (e.g., chat room)
    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync("ReceiveMessage",
            Context.User?.Identity?.Name, message);
    }

    // Join a group
    public async Task JoinRoom(string roomName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName).SendAsync("UserJoined",
            Context.User?.Identity?.Name);
    }

    // Leave a group
    public async Task LeaveRoom(string roomName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName).SendAsync("UserLeft",
            Context.User?.Identity?.Name);
    }

    // Connection lifecycle events
    public override async Task OnConnectedAsync()
    {
        var userId = Context.User?.Identity?.Name;
        Console.WriteLine($"User connected: {userId}");
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var userId = Context.User?.Identity?.Name;
        Console.WriteLine($"User disconnected: {userId}");
        await base.OnDisconnectedAsync(exception);
    }
}

Step 3: Configure SignalR in Program.cs

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add SignalR with configuration
builder.Services.AddSignalR(options =>
{
    // Enable detailed errors in development
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();

    // Configure timeouts
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);

    // Maximum message size (1MB default)
    options.MaximumReceiveMessageSize = 102400; // 100KB
});

// Add CORS for frontend access
builder.Services.AddCors(options =>
{
    options.AddPolicy("ClientApp", policy =>
    {
        policy.WithOrigins("https://localhost:3000") // Your frontend URL
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // Required for SignalR
    });
});

var app = builder.Build();

app.UseCors("ClientApp");
app.UseAuthentication();
app.UseAuthorization();

// Map SignalR hub
app.MapHub<ChatHub>("/hubs/chat");

app.Run();

Step 4: Send Messages from Outside Hubs

Use IHubContext to send messages from controllers, background services, or event handlers:

// Controllers/NotificationsController.cs
using Microsoft.AspNetCore.SignalR;

[ApiController]
[Route("api/[controller]")]
public class NotificationsController : ControllerBase
{
    private readonly IHubContext<ChatHub> _hubContext;

    public NotificationsController(IHubContext<ChatHub> hubContext)
    {
        _hubContext = hubContext;
    }

    [HttpPost("broadcast")]
    public async Task<IActionResult> BroadcastNotification([FromBody] string message)
    {
        // Send to all connected clients
        await _hubContext.Clients.All.SendAsync("ReceiveNotification", message);
        return Ok();
    }

    [HttpPost("user/{userId}")]
    public async Task<IActionResult> SendToUser(string userId, [FromBody] string message)
    {
        // Send to specific user
        await _hubContext.Clients.User(userId).SendAsync("ReceiveNotification", message);
        return Ok();
    }
}
Real-time messaging and live chat application interface

SignalR Client Setup - JavaScript, React, and TypeScript

SignalR provides official JavaScript/TypeScript client libraries that work with any frontend framework. For Next.js, React, Vue, Angular, or vanilla JavaScript, the setup is similar.

Install Client Library

# Install SignalR JavaScript client
npm install @microsoft/signalr

# TypeScript types included by default

React Hook Implementation

Create a reusable React hook for SignalR connections:

// hooks/useSignalR.ts
import { useEffect, useRef, useState } from 'react';
import * as signalR from '@microsoft/signalr';

export function useSignalR(hubUrl: string, accessToken?: string) {
  const [connection, setConnection] = useState<signalR.HubConnection | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const connectionRef = useRef<signalR.HubConnection | null>(null);

  useEffect(() => {
    // Build connection
    const newConnection = new signalR.HubConnectionBuilder()
      .withUrl(hubUrl, {
        accessTokenFactory: () => accessToken || '',
        transport: signalR.HttpTransportType.WebSockets |
                   signalR.HttpTransportType.ServerSentEvents |
                   signalR.HttpTransportType.LongPolling,
      })
      .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: (retryContext) => {
          // Exponential backoff: 0s, 2s, 10s, 30s, then 30s
          if (retryContext.previousRetryCount === 0) return 0;
          if (retryContext.previousRetryCount === 1) return 2000;
          if (retryContext.previousRetryCount === 2) return 10000;
          return 30000;
        },
      })
      .configureLogging(signalR.LogLevel.Information)
      .build();

    connectionRef.current = newConnection;
    setConnection(newConnection);

    // Start connection
    newConnection
      .start()
      .then(() => {
        console.log('SignalR connected');
        setIsConnected(true);
      })
      .catch((err) => console.error('SignalR connection failed:', err));

    // Connection state events
    newConnection.onreconnecting((error) => {
      console.log('SignalR reconnecting...', error);
      setIsConnected(false);
    });

    newConnection.onreconnected((connectionId) => {
      console.log('SignalR reconnected:', connectionId);
      setIsConnected(true);
    });

    newConnection.onclose((error) => {
      console.log('SignalR connection closed:', error);
      setIsConnected(false);
    });

    // Cleanup
    return () => {
      newConnection.stop();
    };
  }, [hubUrl, accessToken]);

  return { connection, isConnected };
}

React Chat Component

// components/Chat.tsx
'use client';

import { useState, useEffect } from 'react';
import { useSignalR } from '@/hooks/useSignalR';

interface Message {
  user: string;
  text: string;
  timestamp: Date;
}

export default function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputMessage, setInputMessage] = useState('');
  const { connection, isConnected } = useSignalR('https://localhost:5001/hubs/chat');

  useEffect(() => {
    if (!connection) return;

    // Listen for incoming messages
    connection.on('ReceiveMessage', (user: string, message: string) => {
      setMessages((prev) => [
        ...prev,
        { user, text: message, timestamp: new Date() },
      ]);
    });

    // Cleanup listener
    return () => {
      connection.off('ReceiveMessage');
    };
  }, [connection]);

  const sendMessage = async () => {
    if (!connection || !inputMessage.trim()) return;

    try {
      await connection.invoke('SendMessage', 'CurrentUser', inputMessage);
      setInputMessage('');
    } catch (err) {
      console.error('Failed to send message:', err);
    }
  };

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      {/* Connection Status */}
      <div className={`mb-4 px-4 py-2 rounded-lg ${
        isConnected ? 'bg-green-900/30 text-green-400' : 'bg-red-900/30 text-red-400'
      }`}>
        {isConnected ? '✓ Connected' : '⚠ Disconnected'}
      </div>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto space-y-3 mb-4">
        {messages.map((msg, index) => (
          <div key={index} className="bg-brand-card rounded-lg p-4">
            <div className="text-brand-accent font-semibold">{msg.user}</div>
            <div className="text-brand-text-primary">{msg.text}</div>
            <div className="text-xs text-brand-text-secondary mt-1">
              {msg.timestamp.toLocaleTimeString()}
            </div>
          </div>
        ))}
      </div>

      {/* Input */}
      <div className="flex gap-2">
        <input
          type="text"
          value={inputMessage}
          onChange={(e) => setInputMessage(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type a message..."
          className="flex-1 bg-brand-card border border-brand-border rounded-lg px-4 py-2"
          disabled={!isConnected}
        />
        <button
          onClick={sendMessage}
          disabled={!isConnected || !inputMessage.trim()}
          className="px-6 py-2 bg-brand-accent text-white rounded-lg disabled:opacity-50"
        >
          Send
        </button>
      </div>
    </div>
  );
}

Vanilla JavaScript Client

For non-React applications or vanilla JavaScript:

// app.js
import * as signalR from '@microsoft/signalr';

// Create connection
const connection = new signalR.HubConnectionBuilder()
  .withUrl('https://localhost:5001/hubs/chat')
  .withAutomaticReconnect()
  .build();

// Listen for messages
connection.on('ReceiveMessage', (user, message) => {
  console.log(`${user}: ${message}`);
  displayMessage(user, message);
});

// Start connection
connection.start()
  .then(() => console.log('Connected to SignalR'))
  .catch(err => console.error('Connection failed:', err));

// Send message
document.getElementById('sendButton').addEventListener('click', async () => {
  const message = document.getElementById('messageInput').value;
  await connection.invoke('SendMessage', 'User', message);
});

Scaling SignalR with Redis Backplane

When you scale SignalR to multiple servers, each server has its own set of connections. Without a backplane, messages sent from one server won't reach clients connected to other servers. Redis backplane solves this using pub/sub to broadcast messages across all servers.

Why Use Redis Backplane?

  • Horizontal scaling: Add more servers to handle increased load
  • Message broadcasting: Messages reach all clients regardless of server
  • High availability: Connection failures on one server don't affect others
  • Load balancing: Distribute connections across multiple instances

Configure Redis Backplane

// Program.cs
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

// Add SignalR with Redis backplane
builder.Services.AddSignalR()
    .AddStackExchangeRedis(connectionString: "localhost:6379", options =>
    {
        options.Configuration.ChannelPrefix = "MyApp";
        options.Configuration.AbortOnConnectFail = false;

        // Connection retry
        options.Configuration.ConnectRetry = 3;
        options.Configuration.ConnectTimeout = 5000;

        // SSL for production
        options.Configuration.Ssl = builder.Environment.IsProduction();
    });

var app = builder.Build();
app.MapHub<ChatHub>("/hubs/chat");
app.Run();

Redis Configuration for Production

Use Azure Cache for Redis or AWS ElastiCache for managed Redis:

// appsettings.Production.json
{
  "Redis": {
    "ConnectionString": "your-azure-redis.redis.cache.windows.net:6380,password=yourkey,ssl=True,abortConnect=False"
  }
}

// Program.cs
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");

builder.Services.AddSignalR()
    .AddStackExchangeRedis(redisConnectionString, options =>
    {
        options.Configuration.ChannelPrefix = "SignalR";
    });

Load Balancer Configuration

Configure sticky sessions (session affinity) for optimal performance:

// Azure Application Gateway or Load Balancer
// Enable cookie-based affinity

// NGINX example
upstream signalr_backend {
    ip_hash; # Sticky sessions based on IP
    server server1.example.com:5000;
    server server2.example.com:5000;
    server server3.example.com:5000;
}

server {
    location /hubs/ {
        proxy_pass http://signalr_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}

Performance Numbers

Based on Microsoft's scaling documentation:

  • • Single server: 5,000-10,000 concurrent connections (Standard_D2s_v3)
  • • With Redis backplane: 100,000+ connections across 10-20 servers
  • • Azure SignalR Service: Millions of connections (fully managed)
  • • Redis adds 1-3ms latency overhead for message broadcasting

Authentication with JWT and Azure AD

Securing SignalR connections is critical for production applications. Use JWT tokens or Azure AD to authenticate users before allowing hub access.

Server-Side JWT Authentication

// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Configure JWT authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
    };

    // Configure JWT for SignalR (query string token)
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];

            // If the request is for our hub...
            var path = context.HttpContext.Request.Path;
            if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
            {
                context.Token = accessToken;
            }
            return Task.CompletedTask;
        }
    };
});

builder.Services.AddAuthorization();
builder.Services.AddSignalR();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapHub<ChatHub>("/hubs/chat");
app.Run();

Authorized Hub with Claims

// Hubs/ChatHub.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;

[Authorize] // Require authentication for all hub methods
public class ChatHub : Hub
{
    [Authorize(Roles = "Admin")] // Specific role required
    public async Task BroadcastAdminMessage(string message)
    {
        await Clients.All.SendAsync("ReceiveAdminMessage", message);
    }

    public async Task SendMessage(string message)
    {
        // Get user identity from claims
        var userId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        var userName = Context.User?.FindFirst(ClaimTypes.Name)?.Value;
        var userEmail = Context.User?.FindFirst(ClaimTypes.Email)?.Value;

        // Validate user permissions
        if (!IsUserAllowedToSendMessages(userId))
        {
            throw new HubException("User not authorized to send messages");
        }

        await Clients.All.SendAsync("ReceiveMessage", userName, message);
    }

    private bool IsUserAllowedToSendMessages(string? userId)
    {
        // Custom authorization logic
        return !string.IsNullOrEmpty(userId);
    }
}

Client-Side JWT Configuration

// React/TypeScript client
import * as signalR from '@microsoft/signalr';

// Get JWT token from your auth system
const getAccessToken = async (): Promise<string> => {
  // Example: retrieve from localStorage, auth context, or cookie
  const token = localStorage.getItem('access_token');
  if (!token) throw new Error('No access token');
  return token;
};

// Create authenticated connection
const connection = new signalR.HubConnectionBuilder()
  .withUrl('https://api.example.com/hubs/chat', {
    accessTokenFactory: () => getAccessToken(),
    transport: signalR.HttpTransportType.WebSockets,
  })
  .withAutomaticReconnect()
  .build();

// Handle authentication failures
connection.onclose((error) => {
  if (error?.message.includes('401')) {
    console.error('Authentication failed, redirecting to login...');
    // Redirect to login page
    window.location.href = '/login';
  }
});

await connection.start();

Azure AD Authentication

For enterprise applications, use Azure AD for authentication:

// Program.cs - Azure AD configuration
using Microsoft.Identity.Web;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

// appsettings.json
{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "yourdomain.onmicrosoft.com",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id"
  }
}
WebSocket communication and network connectivity visualization

Azure SignalR Service - Fully Managed Scaling

Azure SignalR Service is a fully managed service that handles all infrastructure, scaling, and connection management for you. It's the easiest way to scale SignalR to millions of connections without managing Redis or load balancers.

Why Azure SignalR Service:

  • Auto-scaling – Handles 1 to 1,000,000+ concurrent connections automatically
  • Global distribution – Deploy across multiple regions for low latency
  • Zero infrastructure – No Redis, no load balancers, no server management
  • 99.9% SLA – Enterprise-grade reliability and uptime
  • Serverless mode – Pay only for actual usage, not idle capacity

Setup Azure SignalR Service

# Install Azure SignalR SDK
dotnet add package Microsoft.Azure.SignalR

# Program.cs - Configure Azure SignalR
using Microsoft.Azure.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = builder.Configuration["Azure:SignalR:ConnectionString"];

        // Serverless mode (no app server, pure Azure Functions)
        options.ServiceMode = ServiceMode.Serverless;

        // Or Classic mode (app server + Azure SignalR)
        // options.ServiceMode = ServiceMode.Classic;
    });

var app = builder.Build();
app.MapHub<ChatHub>("/hubs/chat");
app.Run();

Azure SignalR with Azure Functions (Serverless)

Go completely serverless with Azure Functions:

// BroadcastFunction.cs
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.SignalRService;

public static class BroadcastFunction
{
    [FunctionName("broadcast")]
    public static async Task Run(
        [TimerTrigger("*/5 * * * * *")] TimerInfo timer,
        [SignalR(HubName = "chat")] IAsyncCollector<SignalRMessage> signalRMessages)
    {
        await signalRMessages.AddAsync(new SignalRMessage
        {
            Target = "newMessage",
            Arguments = new[] { $"Server time: {DateTime.Now}" }
        });
    }

    [FunctionName("negotiate")]
    public static SignalRConnectionInfo Negotiate(
        [HttpTrigger(AuthorizationLevel.Anonymous)] HttpRequest req,
        [SignalRConnectionInfo(HubName = "chat")] SignalRConnectionInfo connectionInfo)
    {
        return connectionInfo;
    }
}

Pricing and Performance Tiers

Free Tier

  • • 20 concurrent connections
  • • 20,000 messages/day
  • • Perfect for development
  • • $0/month

Standard Tier

  • • 1,000 units (1000 connections/unit)
  • • 1M messages/unit/day
  • • Auto-scaling
  • • ~$50/month per unit

Premium Tier

  • • 200,000+ connections
  • • Geo-replication
  • • Private endpoints
  • • ~$250/month per unit

When to Use Azure SignalR Service vs Self-Hosted

Use Azure SignalR if:

  • ✓ Need to scale beyond 10,000 connections
  • ✓ Want zero infrastructure management
  • ✓ Require global distribution
  • ✓ Need 99.9% SLA guarantee

Self-host with Redis if:

  • ✓ Small to medium scale (under 50K connections)
  • ✓ Already have Redis infrastructure
  • ✓ Need full control over infrastructure
  • ✓ Cost-sensitive (self-hosting cheaper at low scale)

Production Best Practices and Performance Optimization

Running SignalR in production requires careful configuration for performance, reliability, and security. Here are proven patterns from real-world deployments.

Connection Management

// Program.cs - Production timeouts and limits
builder.Services.AddSignalR(options =>
{
    // Client timeout (disconnect if no ping)
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);

    // Keep-alive interval (send ping)
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);

    // Maximum message size (prevent abuse)
    options.MaximumReceiveMessageSize = 32 * 1024; // 32KB

    // Enable detailed errors only in development
    options.EnableDetailedErrors = false;

    // Maximum parallel invocations per connection
    options.MaximumParallelInvocationsPerClient = 1;

    // Streaming buffer size
    options.StreamBufferCapacity = 10;
});

Message Compression and Optimization

// Enable message compression (reduces bandwidth by 60-80%)
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = false;
})
.AddMessagePackProtocol(); // Binary protocol, smaller than JSON

// Install package
// dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack

// Client-side MessagePack
import * as signalR from '@microsoft/signalr';
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack';

const connection = new signalR.HubConnectionBuilder()
    .withUrl('/hubs/chat')
    .withHubProtocol(new MessagePackHubProtocol())
    .build();

Rate Limiting and Throttling

// Prevent message flooding and abuse
public class ChatHub : Hub
{
    private static readonly Dictionary<string, Queue<DateTime>> _messageTimes = new();
    private static readonly int MaxMessagesPerMinute = 20;

    public async Task SendMessage(string message)
    {
        var userId = Context.User?.Identity?.Name ?? Context.ConnectionId;

        // Rate limiting check
        if (!IsWithinRateLimit(userId))
        {
            throw new HubException("Rate limit exceeded. Please slow down.");
        }

        await Clients.All.SendAsync("ReceiveMessage", userId, message);
    }

    private bool IsWithinRateLimit(string userId)
    {
        var now = DateTime.UtcNow;

        if (!_messageTimes.ContainsKey(userId))
        {
            _messageTimes[userId] = new Queue<DateTime>();
        }

        var times = _messageTimes[userId];

        // Remove messages older than 1 minute
        while (times.Count > 0 && (now - times.Peek()).TotalMinutes > 1)
        {
            times.Dequeue();
        }

        if (times.Count >= MaxMessagesPerMinute)
        {
            return false;
        }

        times.Enqueue(now);
        return true;
    }
}

Monitoring and Diagnostics

// Application Insights integration
builder.Services.AddApplicationInsightsTelemetry();

// Custom metrics for SignalR
public class ChatHub : Hub
{
    private readonly TelemetryClient _telemetry;

    public ChatHub(TelemetryClient telemetry)
    {
        _telemetry = telemetry;
    }

    public override async Task OnConnectedAsync()
    {
        _telemetry.TrackEvent("SignalR_Connected", new Dictionary<string, string>
        {
            { "UserId", Context.User?.Identity?.Name },
            { "ConnectionId", Context.ConnectionId },
            { "Transport", Context.Features.Get<IHttpTransportFeature>()?.TransportType.ToString() }
        });

        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (exception != null)
        {
            _telemetry.TrackException(exception);
        }

        _telemetry.TrackEvent("SignalR_Disconnected");
        await base.OnDisconnectedAsync(exception);
    }
}

Security Headers and HTTPS

// Program.cs - Security configuration
app.UseHsts(); // Enforce HTTPS
app.UseHttpsRedirection();

// Security headers
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
    context.Response.Headers.Add("X-Frame-Options", "DENY");
    context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
    context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");

    await next();
});

// CORS for production
builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
    {
        policy.WithOrigins("https://yourdomain.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials()
              .SetIsOriginAllowedToAllowWildcardSubdomains();
    });
});

Real-World Use Cases and Production Examples

SignalR powers real-time features in applications across every industry. Here are proven patterns and architecture examples from production systems.

Live Chat and Customer Support

Architecture: SignalR + Redis backplane + Azure SQL Database

  • Scale: 50,000 concurrent conversations, 500K daily active users
  • Features: Typing indicators, read receipts, file sharing, message search
  • Performance: Sub-100ms message delivery, 99.95% uptime
  • Tech stack: ASP.NET Core 8, React, Redis Cluster, Azure SQL
  • Challenges solved: Message persistence, offline delivery, connection recovery

Example: Zendesk, Intercom, Microsoft Teams

Real-Time Trading Platform

Architecture: Azure SignalR Service + Azure Functions + Cosmos DB

  • Scale: 2 million concurrent users, 100K trades/second
  • Features: Live price feeds, order book updates, trade execution notifications
  • Performance: 5-15ms latency end-to-end, guaranteed message ordering
  • Tech stack: .NET 8, Next.js, Azure SignalR Premium, Cosmos DB
  • Challenges solved: Message throttling, connection affinity, regional failover

Example: Robinhood, TD Ameritrade, E*TRADE

Collaborative Document Editing

Architecture: SignalR + Operational Transform (OT) + PostgreSQL

  • Scale: 100 concurrent editors per document, 1M documents
  • Features: Real-time cursors, presence indicators, conflict resolution
  • Performance: 50ms change propagation, no merge conflicts
  • Tech stack: ASP.NET Core, Monaco Editor, SignalR, PostgreSQL
  • Challenges solved: OT algorithm, state synchronization, undo/redo

Example: Google Docs, Notion, Figma

IoT Device Monitoring Dashboard

Architecture: SignalR + Azure IoT Hub + TimeSeries Insights

  • Scale: 1 million IoT devices, 10K dashboard users
  • Features: Live telemetry, alerts, device control, historical charts
  • Performance: 1-second telemetry updates, 500ms alert delivery
  • Tech stack: ASP.NET Core, Angular, Azure IoT Hub, InfluxDB
  • Challenges solved: Data aggregation, connection throttling, offline buffering

Example: Azure IoT Central, AWS IoT Core dashboards

Multiplayer Game Backend

Architecture: SignalR + Azure PlayFab + Redis

  • Scale: 50K concurrent game sessions, 500K daily players
  • Features: Player movements, game state sync, matchmaking, leaderboards
  • Performance: 20-30ms tick rate, deterministic simulation
  • Tech stack: .NET 8, Unity WebGL, SignalR, Redis
  • Challenges solved: Client prediction, server reconciliation, lag compensation

Example: Fall Guys, Among Us (Unity + SignalR implementations)

Performance Benchmarks

Based on Microsoft's official benchmarks and real production data:

Self-Hosted (Standard_D4s_v3)

  • • 10,000 concurrent connections
  • • 50,000 messages/second broadcast
  • • 2-5ms server processing latency
  • • ~$150/month Azure compute cost

Azure SignalR Service (Standard)

  • • 1,000,000+ concurrent connections
  • • 1M+ messages/second broadcast
  • • 1-3ms service latency
  • • ~$50-500/month depending on units

Frequently Asked Questions

What is SignalR and why should I use it in 2025?

SignalR is a real-time communication library for ASP.NET that simplifies adding real-time web functionality. It automatically handles connection management, fallback transports (WebSockets, Server-Sent Events, Long Polling), and scaling. In 2025, it's the industry-standard choice for .NET real-time apps due to Azure SignalR Service integration, excellent performance, and built-in scaling support.

How do I scale SignalR to handle millions of connections?

Scale SignalR using a Redis backplane or Azure SignalR Service. Redis backplane allows multiple servers to share connection state via pub/sub, enabling horizontal scaling. Azure SignalR Service is fully managed and scales automatically to millions of connections with built-in load balancing and geo-distribution. For large deployments, combine sticky sessions, connection pooling, and message compression.

What's the difference between WebSockets, Server-Sent Events, and Long Polling?

WebSockets provide full-duplex bidirectional communication with lowest latency, best for real-time apps. Server-Sent Events (SSE) are unidirectional (server to client only) using HTTP, simpler and work through proxies. Long Polling is the fallback where clients repeatedly poll the server, highest latency but maximum compatibility. SignalR automatically negotiates the best available transport.

How do I secure SignalR with JWT authentication?

Configure JWT authentication in ASP.NET Core, then pass the token in SignalR connection options using accessTokenFactory. On the server, use [Authorize] attribute on hubs and access user claims via Context.User. Validate tokens on every connection and implement authorization policies for specific hub methods. Use HTTPS/WSS in production and implement connection throttling to prevent abuse.

Can I use SignalR with React, Angular, or Vue.js frontends?

Yes, SignalR provides JavaScript client libraries that work with any frontend framework. Use @microsoft/signalr npm package for React, Angular, Vue, or vanilla JavaScript. The client API is framework-agnostic and integrates easily with state management (Redux, Zustand, Pinia). SignalR also has native clients for .NET, Java, and Swift.

Should I use self-hosted SignalR with Redis or Azure SignalR Service?

Use Azure SignalR Service if you need to scale beyond 10,000 connections, want zero infrastructure management, require global distribution, or need 99.9% SLA. Self-host with Redis if you're at small to medium scale (under 50K connections), already have Redis infrastructure, need full control, or are cost-sensitive (self-hosting is cheaper at low scale). Azure SignalR Service pricing starts at $50/month for 1,000 units (1M connections).

Ready to Build Real-Time Applications?

SignalR is the proven solution for real-time web communication in the .NET ecosystem. With automatic transport negotiation, built-in scaling via Azure SignalR Service, and production-grade reliability, it's the fastest path to building chat applications, live dashboards, collaborative tools, and real-time data feeds.

The framework handles the hard parts – connection management, transport selection, scaling infrastructure – so you can focus on building great user experiences. Whether you're building a startup MVP or scaling to millions of users, SignalR provides the foundation for success. Check out our cloud solutions guide for deployment strategies.

Need Help Building Real-Time Features?

We specialize in building production-ready real-time applications with SignalR, Azure SignalR Service, and modern frontend frameworks. Experts in WebSockets, scaling strategies, authentication, and cloud deployment. From chat applications to live dashboards and collaborative tools, let's build something amazing together.

Related Articles

Building Real-Time Apps with SignalR and Azure 2025 | Wojciechowski.app | Wojciechowski.app