SignalR
Trigger On
- building chat, notification, collaboration, or live-update features
- debugging hub lifetime, connection state, or transport issues
- deciding whether SignalR or another transport better fits the scenario
- implementing real-time broadcasting to groups of connected clients
- scaling SignalR across multiple servers
Documentation
References
- patterns.md - Detailed hub patterns, streaming, groups, presence, and advanced messaging techniques
- anti-patterns.md - Common SignalR mistakes and how to avoid them
Workflow
- Use SignalR for broadcast-style or connection-oriented real-time features; do not force gRPC into hub-style fan-out scenarios.
- Model hub contracts intentionally and keep hub methods thin, delegating durable work elsewhere.
- Plan for reconnection, backpressure, auth, and fan-out costs instead of treating real-time messaging as stateless request/response.
- Use groups, presence, and connection metadata deliberately so scale-out behavior is understandable.
- If Native AOT or trimming is in play, validate supported protocols and serialization choices explicitly.
- Test connection behavior and failure modes, not just happy-path message delivery.
Current Upstream Notes
dotnet/aspnetcore v10.0.11 is a servicing release and does not change the SignalR programming model. Keep guidance focused on hub contract design, reconnection, transport, authorization, and scale-out validation.
- The August 2026 ASP.NET Core overview still positions SignalR for real-time server/client communication. After servicing updates, rerun at least one reconnect and group-broadcast smoke path because dependency updates can expose client/server package mismatches.
Hub Patterns
Strongly-Typed Hub (Recommended)
// Define the client interface
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task UserJoined(string user);
Task UserLeft(string user);
}
// Implement the strongly-typed hub
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
// Compiler checks client method calls
await Clients.All.ReceiveMessage(user, message);
}
public override async Task OnConnectedAsync()
{
await Clients.Others.UserJoined(Context.User?.Identity?.Name ?? "Anonymous");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Clients.Others.UserLeft(Context.User?.Identity?.Name ?? "Anonymous");
await base.OnDisconnectedAsync(exception);
}
}
Using Groups for Targeted Messaging
public class NotificationHub : Hub<INotificationClient>
{
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name);
}
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).ReceiveNotification(message);
}
}
Hub Method with Custom Object Parameters (API Versioning)
// Use custom objects to avoid breaking changes
public class SendMessageRequest
{
public string Message { get; set; } = string.Empty;
public string? Recipient { get; set; } // Added later without breaking clients
public int? Priority { get; set; } // Added later without breaking clients
}
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(SendMessageRequest request)
{
// Handle both old and new clients
if (request.Recipient != null)
{
await Clients.User(request.Recipient).ReceiveMessage(request.Message);
}
else
{
await Clients.All.ReceiveMessage(request.Message);
}
}
}
Client Patterns
JavaScript Client with Automatic Reconnection
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Retry delays
.configureLogging(signalR.LogLevel.Information)
.build();
// Handle reconnection events
connection.onreconnecting(error => {
console.log("Reconnecting...", error);
updateUIForReconnecting();
});
connection.onreconnected(connectionId => {
console.log("Reconnected with ID:", connectionId);
// Rejoin groups - reconnection does not restore group membership
rejoinGroups();
updateUIForConnected();
});
connection.onclose(error => {
console.log("Connection closed", error);
updateUIForDisconnected();
});
async function start() {
try {
await connection.start();
console.log("SignalR Connected");
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
}
start();
.NET Client with Reconnection
var connection = new HubConnectionBuilder()
.WithUrl("https://localhost:5001/chatHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(GetAccessToken());
})
.WithAutomaticReconnect()
.Build();
connection.Reconnecting += error =>
{
_logger.LogWarning("Connection lost. Reconnecting: {Error}", error?.Message);
return Task.CompletedTask;
};
connection.Reconnected += connectionId =>
{
_logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);
// Rejoin groups after reconnection
return RejoinGroupsAsync();
};
connection.Closed += async error =>
{
_logger.LogError("Connection closed: {Error}", error?.Message);
await Task.Delay(Random.Shared.Next(0, 5) * 1000);
await connection.StartAsync();
};
await connection.StartAsync();
Server Configuration
Hub Registration with Authentication
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
options.StreamBufferCapacity = 10;
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
})
.AddMessagePackProtocol(); // Binary protocol for performance
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
=>
{
// 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;
}
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<ChatHub>("/hubs/chat");
Sending Messages from Outside a Hub
public class NotificationService
{
private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;
public NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext)
{
_hubContext = hubContext;
}
public async Task NotifyAllAsync(string message)
{
await _hubContext.Clients.All.ReceiveNotification(message);
}
public async Task NotifyUserAsync(string userId, string message)
{
await _hubContext.Clients.User(userId).ReceiveNotification(message);
}
public async Task NotifyGroupAsync(string groupName, string message)
{
await _hubContext.Clients.Group(groupName).ReceiveNotification(message);
}
}
Scaling with Redis Backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis(connectionString, options =>
{
options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
});
Anti-Patterns to Avoid
| Anti-Pattern |
Why It's Bad |
Better Approach |
| Storing state in Hub properties |
Hub instances are created per method call |
Use IMemoryCache, database, or external store |
| Instantiating Hub directly |
Bypasses SignalR infrastructure |
Use IHubContext<THub> for external messaging |
Not awaiting SendAsync calls |
Messages may not be sent before hub method completes |
Always await async hub calls |
| Adding method parameters without versioning |
Breaking change for existing clients |
Use custom object parameters |
| Ignoring reconnection group loss |
Clients lose group membership on reconnect |
Re-add to groups in OnConnectedAsync or client reconnect handler |
| Large payloads over SignalR |
Memory pressure, bandwidth issues |
Use REST/gRPC for bulk data, SignalR for notifications |
| Missing backplane in multi-server |
Messages only reach clients on same server |
Use Redis backplane or Azure SignalR Service |
| Exposing ORM entities directly |
May serialize sensitive data |
Use DTOs with explicit properties |
| Not validating incoming messages |
Security risk after initial auth |
Validate every hub method input |
Best Practices
Connection Management
- Enable automatic reconnection with exponential backoff delays
- Handle group rejoining explicitly after reconnection (connection ID changes)
- Implement heartbeat monitoring on the client to detect stale connections
- Use sticky sessions when scaling across multiple servers (unless using Azure SignalR Service)
Performance
- Use MessagePack protocol for smaller message sizes and faster serialization
- Throttle high-frequency events like typing indicators or mouse movements
- Batch messages when possible instead of many small sends
- Set appropriate buffer sizes based on expected message throughput
Security
- Authenticate at connection time using JWT tokens via query string
- Authorize hub methods using
[Authorize] attribute
- Validate all incoming messages even after authentication
- Use HTTPS for all SignalR connections
API Design
- Use strongly-typed hubs to catch client method name typos at compile time
- Use custom object parameters to enable backward-compatible API evolution
- Version hub names (e.g.,
ChatHubV2) for breaking changes
- Keep hub methods thin and delegate business logic to services
Observability
- Log connection events (connect, disconnect, reconnect)
- Track transport type used by each connection
- Monitor message delivery latency and failure rates
- Integrate with Application Insights or other APM tools
Deliver
- clear hub contracts and connection behavior
- real-time delivery that matches the product scenario
- validation for reconnection and authorization flows
- appropriate scale-out strategy for multi-server deployments
Validate
- SignalR is the correct transport for the use case
- hub methods remain orchestration-oriented
- group and auth behavior are explicit and tested
- reconnection and group membership are handled correctly
- backplane is configured for multi-server scenarios
- message validation is implemented in hub methods
1---2name: signalr3description: Implement or review SignalR hubs, streaming, reconnection, transport, and real-time delivery patterns in ASP.NET Core applications. USE FOR: building chat, notification, collaboration, or live-update features; debugging hub lifetime, connection state, or transport issues; deciding whether SignalR or another. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or validation commands when changes are made.4---56# SignalR78## Trigger On910- building chat, notification, collaboration, or live-update features11- debugging hub lifetime, connection state, or transport issues12- deciding whether SignalR or another transport better fits the scenario13- implementing real-time broadcasting to groups of connected clients14- scaling SignalR across multiple servers1516## Documentation1718- [ASP.NET Core SignalR Overview](https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-10.0)19- [SignalR Hubs](https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-10.0)20- [SignalR API Design Considerations](https://learn.microsoft.com/en-us/aspnet/core/signalr/api-design?view=aspnetcore-10.0)21- [SignalR Production Hosting and Scaling](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-10.0)22- [SignalR Configuration](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-10.0)2324### References2526- [patterns.md](references/patterns.md) - Detailed hub patterns, streaming, groups, presence, and advanced messaging techniques27- [anti-patterns.md](references/anti-patterns.md) - Common SignalR mistakes and how to avoid them2829## Workflow30311. Use SignalR for broadcast-style or connection-oriented real-time features; do not force gRPC into hub-style fan-out scenarios.322. Model hub contracts intentionally and keep hub methods thin, delegating durable work elsewhere.333. Plan for reconnection, backpressure, auth, and fan-out costs instead of treating real-time messaging as stateless request/response.344. Use groups, presence, and connection metadata deliberately so scale-out behavior is understandable.355. If Native AOT or trimming is in play, validate supported protocols and serialization choices explicitly.366. Test connection behavior and failure modes, not just happy-path message delivery.3738## Current Upstream Notes3940- `dotnet/aspnetcore` `v10.0.11` is a servicing release and does not change the SignalR programming model. Keep guidance focused on hub contract design, reconnection, transport, authorization, and scale-out validation.41- The August 2026 ASP.NET Core overview still positions SignalR for real-time server/client communication. After servicing updates, rerun at least one reconnect and group-broadcast smoke path because dependency updates can expose client/server package mismatches.4243## Hub Patterns4445### Strongly-Typed Hub (Recommended)46```csharp47// Define the client interface48public interface IChatClient49{50 Task ReceiveMessage(string user, string message);51 Task UserJoined(string user);52 Task UserLeft(string user);53}5455// Implement the strongly-typed hub56public class ChatHub : Hub<IChatClient>57{58 public async Task SendMessage(string user, string message)59 {60 // Compiler checks client method calls61 await Clients.All.ReceiveMessage(user, message);62 }6364 public override async Task OnConnectedAsync()65 {66 await Clients.Others.UserJoined(Context.User?.Identity?.Name ?? "Anonymous");67 await base.OnConnectedAsync();68 }6970 public override async Task OnDisconnectedAsync(Exception? exception)71 {72 await Clients.Others.UserLeft(Context.User?.Identity?.Name ?? "Anonymous");73 await base.OnDisconnectedAsync(exception);74 }75}76```7778### Using Groups for Targeted Messaging79```csharp80public class NotificationHub : Hub<INotificationClient>81{82 public async Task JoinGroup(string groupName)83 {84 await Groups.AddToGroupAsync(Context.ConnectionId, groupName);85 await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name);86 }8788 public async Task LeaveGroup(string groupName)89 {90 await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);91 }9293 public async Task SendToGroup(string groupName, string message)94 {95 await Clients.Group(groupName).ReceiveNotification(message);96 }97}98```99100### Hub Method with Custom Object Parameters (API Versioning)101```csharp102// Use custom objects to avoid breaking changes103public class SendMessageRequest104{105 public string Message { get; set; } = string.Empty;106 public string? Recipient { get; set; } // Added later without breaking clients107 public int? Priority { get; set; } // Added later without breaking clients108}109110public class ChatHub : Hub<IChatClient>111{112 public async Task SendMessage(SendMessageRequest request)113 {114 // Handle both old and new clients115 if (request.Recipient != null)116 {117 await Clients.User(request.Recipient).ReceiveMessage(request.Message);118 }119 else120 {121 await Clients.All.ReceiveMessage(request.Message);122 }123 }124}125```126127## Client Patterns128129### JavaScript Client with Automatic Reconnection130```javascript131const connection = new signalR.HubConnectionBuilder()132 .withUrl("/chatHub")133 .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Retry delays134 .configureLogging(signalR.LogLevel.Information)135 .build();136137// Handle reconnection events138connection.onreconnecting(error => {139 console.log("Reconnecting...", error);140 updateUIForReconnecting();141});142143connection.onreconnected(connectionId => {144 console.log("Reconnected with ID:", connectionId);145 // Rejoin groups - reconnection does not restore group membership146 rejoinGroups();147 updateUIForConnected();148});149150connection.onclose(error => {151 console.log("Connection closed", error);152 updateUIForDisconnected();153});154155async function start() {156 try {157 await connection.start();158 console.log("SignalR Connected");159 } catch (err) {160 console.log(err);161 setTimeout(start, 5000);162 }163}164165start();166```167168### .NET Client with Reconnection169```csharp170var connection = new HubConnectionBuilder()171 .WithUrl("https://localhost:5001/chatHub", options =>172 {173 options.AccessTokenProvider = () => Task.FromResult(GetAccessToken());174 })175 .WithAutomaticReconnect()176 .Build();177178connection.Reconnecting += error =>179{180 _logger.LogWarning("Connection lost. Reconnecting: {Error}", error?.Message);181 return Task.CompletedTask;182};183184connection.Reconnected += connectionId =>185{186 _logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);187 // Rejoin groups after reconnection188 return RejoinGroupsAsync();189};190191connection.Closed += async error =>192{193 _logger.LogError("Connection closed: {Error}", error?.Message);194 await Task.Delay(Random.Shared.Next(0, 5) * 1000);195 await connection.StartAsync();196};197198await connection.StartAsync();199```200201## Server Configuration202203### Hub Registration with Authentication204```csharp205var builder = WebApplication.CreateBuilder(args);206207builder.Services.AddSignalR(options =>208{209 options.EnableDetailedErrors = builder.Environment.IsDevelopment();210 options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB211 options.StreamBufferCapacity = 10;212 options.KeepAliveInterval = TimeSpan.FromSeconds(15);213 options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);214})215.AddMessagePackProtocol(); // Binary protocol for performance216217builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)218 .AddJwtBearer(options =>219 {220 options.Events = new JwtBearerEvents221 {222 OnMessageReceived = context =>223 {224 // Read token from query string for WebSocket connections225 var accessToken = context.Request.Query["access_token"];226 var path = context.HttpContext.Request.Path;227 if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))228 {229 context.Token = accessToken;230 }231 return Task.CompletedTask;232 }233 };234 });235236var app = builder.Build();237238app.UseAuthentication();239app.UseAuthorization();240241app.MapHub<ChatHub>("/hubs/chat");242```243244### Sending Messages from Outside a Hub245```csharp246public class NotificationService247{248 private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;249250 public NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext)251 {252 _hubContext = hubContext;253 }254255 public async Task NotifyAllAsync(string message)256 {257 await _hubContext.Clients.All.ReceiveNotification(message);258 }259260 public async Task NotifyUserAsync(string userId, string message)261 {262 await _hubContext.Clients.User(userId).ReceiveNotification(message);263 }264265 public async Task NotifyGroupAsync(string groupName, string message)266 {267 await _hubContext.Clients.Group(groupName).ReceiveNotification(message);268 }269}270```271272## Scaling with Redis Backplane273274```csharp275builder.Services.AddSignalR()276 .AddStackExchangeRedis(connectionString, options =>277 {278 options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");279 });280```281282## Anti-Patterns to Avoid283284| Anti-Pattern | Why It's Bad | Better Approach |285|--------------|--------------|-----------------|286| Storing state in Hub properties | Hub instances are created per method call | Use `IMemoryCache`, database, or external store |287| Instantiating Hub directly | Bypasses SignalR infrastructure | Use `IHubContext<THub>` for external messaging |288| Not awaiting `SendAsync` calls | Messages may not be sent before hub method completes | Always `await` async hub calls |289| Adding method parameters without versioning | Breaking change for existing clients | Use custom object parameters |290| Ignoring reconnection group loss | Clients lose group membership on reconnect | Re-add to groups in `OnConnectedAsync` or client reconnect handler |291| Large payloads over SignalR | Memory pressure, bandwidth issues | Use REST/gRPC for bulk data, SignalR for notifications |292| Missing backplane in multi-server | Messages only reach clients on same server | Use Redis backplane or Azure SignalR Service |293| Exposing ORM entities directly | May serialize sensitive data | Use DTOs with explicit properties |294| Not validating incoming messages | Security risk after initial auth | Validate every hub method input |295296## Best Practices297298### Connection Management2991. **Enable automatic reconnection** with exponential backoff delays3002. **Handle group rejoining** explicitly after reconnection (connection ID changes)3013. **Implement heartbeat monitoring** on the client to detect stale connections3024. **Use sticky sessions** when scaling across multiple servers (unless using Azure SignalR Service)303304### Performance3051. **Use MessagePack protocol** for smaller message sizes and faster serialization3062. **Throttle high-frequency events** like typing indicators or mouse movements3073. **Batch messages** when possible instead of many small sends3084. **Set appropriate buffer sizes** based on expected message throughput309310### Security3111. **Authenticate at connection time** using JWT tokens via query string3122. **Authorize hub methods** using `[Authorize]` attribute3133. **Validate all incoming messages** even after authentication3144. **Use HTTPS** for all SignalR connections315316### API Design3171. **Use strongly-typed hubs** to catch client method name typos at compile time3182. **Use custom object parameters** to enable backward-compatible API evolution3193. **Version hub names** (e.g., `ChatHubV2`) for breaking changes3204. **Keep hub methods thin** and delegate business logic to services321322### Observability3231. **Log connection events** (connect, disconnect, reconnect)3242. **Track transport type** used by each connection3253. **Monitor message delivery** latency and failure rates3264. **Integrate with Application Insights** or other APM tools327328## Deliver329330- clear hub contracts and connection behavior331- real-time delivery that matches the product scenario332- validation for reconnection and authorization flows333- appropriate scale-out strategy for multi-server deployments334335## Validate336337- SignalR is the correct transport for the use case338- hub methods remain orchestration-oriented339- group and auth behavior are explicit and tested340- reconnection and group membership are handled correctly341- backplane is configured for multi-server scenarios342- message validation is implemented in hub methods