SignalR is a real-time communication library for ASP.NET Core that lets a server push messages to connected clients instantly, without the client polling for updates. It negotiates the best available transport automatically — WebSockets first, falling back to Server-Sent Events or long polling — and manages connections, reconnection, and message routing for you. Code is organized around hubs: server-side classes whose methods clients can invoke, and which can broadcast to all clients, a single user, or named groups. SignalR includes built-in authentication and authorization support, strongly-typed hubs for compile-time safety, and a Redis backplane for scaling across multiple server instances. It powers the features users now expect by default: live chat, notifications, presence indicators, and real-time dashboards. This guide walks through hubs, groups, JWT authentication, sending from services with IHubContext, the JavaScript client, scaling with Redis, and production tuning.
What is SignalR?
SignalR capabilities:
- Automatic transport negotiation (WebSockets, Server-Sent Events, Long Polling)
- Connection management and reconnection handling
- Group-based messaging
- Strongly-typed hubs for type safety
- Built-in authentication and authorization
- Scales with Redis backplane for multiple servers
Basic Setup
// Program.cs
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/hubs/chat");
app.MapHub<NotificationHub>("/hubs/notifications");
app.Run();How Do You Create a Hub?
Basic Chat Hub
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
public ChatHub(ILogger<ChatHub> logger)
{
_logger = logger;
}
// Called when a client connects
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
await base.OnConnectedAsync();
}
// Called when a client disconnects
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
// Client can call this method
public async Task SendMessage(string user, string message)
{
_logger.LogInformation("Message from {User}: {Message}", user, message);
// Send to all connected clients
await Clients.All.SendAsync("ReceiveMessage", user, message, DateTime.UtcNow);
}
// Send to specific user
public async Task SendPrivateMessage(string targetUserId, string message)
{
await Clients.User(targetUserId).SendAsync("ReceivePrivateMessage", Context.UserIdentifier, message);
}
// Send to caller only
public async Task Echo(string message)
{
await Clients.Caller.SendAsync("EchoResponse", message);
}
}Strongly-Typed Hub
// Define client interface
public interface IChatClient
{
Task ReceiveMessage(string user, string message, DateTime timestamp);
Task UserConnected(string connectionId);
Task UserDisconnected(string connectionId);
Task UserJoinedRoom(string user, string room);
Task UserLeftRoom(string user, string room);
}
// Strongly-typed hub
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
// Compile-time type checking!
await Clients.All.ReceiveMessage(user, message, DateTime.UtcNow);
}
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).UserJoinedRoom(Context.UserIdentifier!, roomName);
}
public async Task LeaveRoom(string roomName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).UserLeftRoom(Context.UserIdentifier!, roomName);
}
public async Task SendToRoom(string roomName, string message)
{
await Clients.Group(roomName).ReceiveMessage(Context.UserIdentifier!, message, DateTime.UtcNow);
}
}How Do You Target Users, Groups, and Connections?
public class NotificationHub : Hub
{
// Send to all clients
public async Task BroadcastNotification(string message)
{
await Clients.All.SendAsync("Notification", message);
}
// Send to specific connection
public async Task SendToConnection(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("Notification", message);
}
// Send to specific user (by user ID from claims)
public async Task SendToUser(string userId, string message)
{
await Clients.User(userId).SendAsync("Notification", message);
}
// Send to multiple users
public async Task SendToUsers(IReadOnlyList<string> userIds, string message)
{
await Clients.Users(userIds).SendAsync("Notification", message);
}
// Send to group
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).SendAsync("Notification", message);
}
// Send to all except caller
public async Task SendToOthers(string message)
{
await Clients.Others.SendAsync("Notification", message);
}
// Send to all except specific connections
public async Task SendToAllExcept(IReadOnlyList<string> excludedConnectionIds, string message)
{
await Clients.AllExcept(excludedConnectionIds).SendAsync("Notification", message);
}
}How Do You Authenticate SignalR Connections?
// Program.cs - Configure JWT for SignalR
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings.Issuer,
ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey))
};
// Configure for SignalR
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Read token from query string for WebSocket connections
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
// Secure hub
[Authorize]
public class SecureHub : Hub
{
public string? UserId => Context.UserIdentifier;
public string? UserEmail => Context.User?.FindFirst(ClaimTypes.Email)?.Value;
public override async Task OnConnectedAsync()
{
// User is authenticated here
var userId = Context.UserIdentifier;
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
await base.OnConnectedAsync();
}
}
// Authorize specific methods
public class ChatHub : Hub
{
[Authorize(Roles = "Admin")]
public async Task DeleteMessage(Guid messageId)
{
// Only admins can call this
}
[Authorize(Policy = "PremiumUser")]
public async Task SendPriorityMessage(string message)
{
// Only premium users can call this
}
}How Do You Send Messages from Outside a Hub?
// Inject IHubContext to send messages from services or controllers
public class OrderService : IOrderService
{
private readonly IHubContext<NotificationHub> _hubContext;
public OrderService(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
public async Task CreateOrderAsync(Order order)
{
// Save order...
// Notify the user
await _hubContext.Clients.User(order.UserId.ToString())
.SendAsync("OrderCreated", new
{
OrderId = order.Id,
Status = order.Status,
Total = order.TotalAmount
});
// Notify admins
await _hubContext.Clients.Group("admins")
.SendAsync("NewOrder", order.Id);
}
}
// With strongly-typed hub
public class OrderService : IOrderService
{
private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;
public async Task CreateOrderAsync(Order order)
{
await _hubContext.Clients.User(order.UserId.ToString())
.OrderCreated(order.Id, order.Status);
}
}Client Implementation (JavaScript)
// Install: npm install @microsoft/signalr
import * as signalR from "@microsoft/signalr";
class SignalRService {
private connection: signalR.HubConnection;
constructor() {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/chat", {
accessTokenFactory: () => localStorage.getItem("token") || ""
})
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(signalR.LogLevel.Information)
.build();
this.registerHandlers();
}
private registerHandlers() {
this.connection.on("ReceiveMessage", (user: string, message: string, timestamp: Date) => {
console.log(`${user}: ${message}`);
// Update UI
});
this.connection.on("UserConnected", (connectionId: string) => {
console.log(`User connected: ${connectionId}`);
});
this.connection.onreconnecting((error) => {
console.log("Reconnecting...", error);
});
this.connection.onreconnected((connectionId) => {
console.log("Reconnected:", connectionId);
});
this.connection.onclose((error) => {
console.log("Connection closed", error);
});
}
async start() {
try {
await this.connection.start();
console.log("SignalR Connected");
} catch (err) {
console.error("SignalR Connection Error:", err);
setTimeout(() => this.start(), 5000);
}
}
async sendMessage(user: string, message: string) {
await this.connection.invoke("SendMessage", user, message);
}
async joinRoom(roomName: string) {
await this.connection.invoke("JoinRoom", roomName);
}
async leaveRoom(roomName: string) {
await this.connection.invoke("LeaveRoom", roomName);
}
}
// Usage
const signalr = new SignalRService();
await signalr.start();
await signalr.sendMessage("John", "Hello everyone!");How Do You Scale SignalR with a Redis Backplane?
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis// Program.cs - Add Redis backplane for scaling
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!, options =>
{
options.Configuration.ChannelPrefix = "MyApp";
});
// With this configuration, messages are broadcast across all server instances
// through Redis pub/subReal-World Example: Live Dashboard
public interface IDashboardClient
{
Task MetricsUpdated(DashboardMetrics metrics);
Task OrderReceived(OrderSummary order);
Task AlertTriggered(Alert alert);
}
public class DashboardHub : Hub<IDashboardClient>
{
public override async Task OnConnectedAsync()
{
// Add to dashboard viewers group
await Groups.AddToGroupAsync(Context.ConnectionId, "dashboard");
await base.OnConnectedAsync();
}
}
// Background service to push metrics
public class MetricsPublisher : BackgroundService
{
private readonly IHubContext<DashboardHub, IDashboardClient> _hub;
private readonly IMetricsService _metrics;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var metrics = await _metrics.GetCurrentMetricsAsync();
await _hub.Clients.Group("dashboard").MetricsUpdated(metrics);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
// Order service pushing updates
public class OrderService
{
private readonly IHubContext<DashboardHub, IDashboardClient> _hub;
public async Task CreateOrderAsync(Order order)
{
// Save order...
await _hub.Clients.Group("dashboard").OrderReceived(new OrderSummary
{
Id = order.Id,
Customer = order.CustomerName,
Total = order.TotalAmount,
Timestamp = DateTime.UtcNow
});
}
}Connection Limits and Performance
// Configure connection limits
builder.Services.AddSignalR(options =>
{
options.MaximumReceiveMessageSize = 32 * 1024; // 32KB
options.StreamBufferCapacity = 10;
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.HandshakeTimeout = TimeSpan.FromSeconds(15);
});
// Use MessagePack for better performance
builder.Services.AddSignalR()
.AddMessagePackProtocol();Best Practices
SignalR best practices:
- Use strongly-typed hubs for compile-time safety
- Keep hub methods simple - delegate to services
- Use groups for room-based features
- Implement proper error handling
- Add Redis backplane for multi-server deployments
- Monitor connection counts and message throughput
- Use MessagePack for better performance
- Implement reconnection logic on the client
SignalR abstracts away the complexity of real-time communication. Use it for live dashboards, notifications, chat, and any feature where users expect immediate updates. For high-throughput scenarios, consider gRPC streaming instead.
Real-time features create engaging user experiences. SignalR makes implementing these features straightforward while handling connection management, reconnection, and scaling challenges for you.
