Azure SignalR Service: Real-Time .NET Without the Infrastructure
SignalR is the standard way to build real-time features in .NET — chat, live dashboards, notifications, collaborative editing. It handles WebSocket management, fallback transports, and connection lifecycle for you.
The challenge comes at scale. Each SignalR connection is persistent, consuming memory and sockets on your server. When you scale out to multiple instances, you need a backplane (Redis, typically) to coordinate messages across servers. Azure SignalR Service eliminates both problems by offloading connection management entirely.
How It Works
With Azure SignalR Service, your server doesn't hold client connections directly. Instead:
- Clients connect to the Azure SignalR Service endpoint.
- Your server connects to Azure SignalR Service as a "server connection."
- When you send a message, it goes from your server to the service, which fans it out to connected clients.
Your server handles the business logic and hub methods. Azure handles the thousands of concurrent WebSocket connections.
Setting Up
Install the Microsoft.Azure.SignalR package and add one line to your startup:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR()
.AddAzureSignalR(builder.Configuration["Azure:SignalR:ConnectionString"]);
var app = builder.Build();
app.MapHub<NotificationHub>("/notifications");
app.Run();
Your hub code doesn't change at all:
public class NotificationHub : Hub
{
private readonly ILogger<NotificationHub> _logger;
public NotificationHub(ILogger<NotificationHub> logger) => _logger = logger;
public async Task SendNotification(string channel, string message)
{
_logger.LogInformation("Broadcasting to {Channel}", channel);
await Clients.Group(channel).SendAsync("ReceiveNotification", message);
}
public async Task JoinChannel(string channel)
{
await Groups.AddToGroupAsync(Context.ConnectionId, channel);
await Clients.Caller.SendAsync("Joined", channel);
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client {Id} connected", Context.ConnectionId);
await base.OnConnectedAsync();
}
}
This is identical to self-hosted SignalR. The AddAzureSignalR() call switches the transport layer — everything above it stays the same.
Sending Messages from Outside a Hub
Often you need to push messages from background services or API endpoints, not just in response to hub method calls. Use IHubContext<T>:
public class OrderCompletedHandler
{
private readonly IHubContext<NotificationHub> _hubContext;
public OrderCompletedHandler(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
public async Task HandleAsync(OrderCompletedEvent evt)
{
await _hubContext.Clients
.User(evt.CustomerId)
.SendAsync("OrderCompleted", new
{
evt.OrderId,
evt.Total,
CompletedAt = evt.CompletedAt.ToString("O")
});
}
}
This works exactly as it does with self-hosted SignalR. The Azure service routes the message to the correct client connection.
Serverless Mode
For scenarios where you don't need persistent hub connections from your server — for example, sending notifications from Azure Functions — use serverless mode:
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = builder.Configuration["Azure:SignalR:ConnectionString"];
options.ServerStickyMode = ServerStickyMode.Required;
});
In serverless mode, the service accepts REST API calls to send messages:
public class NotificationFunction
{
[Function("SendAlert")]
public async Task SendAlert(
[ServiceBusTrigger("alerts")] ServiceBusReceivedMessage message,
[SignalROutput(HubName = "notifications")] IAsyncCollector<SignalRMessage> signalR)
{
var alert = message.Body.ToObjectFromJson<Alert>();
await signalR.AddAsync(new SignalRMessage
{
Target = "ReceiveAlert",
Arguments = new object[] { alert }
});
}
}
Strongly Typed Hubs
For better compile-time safety, use strongly typed hubs:
public interface INotificationClient
{
Task ReceiveNotification(string message);
Task OrderCompleted(OrderSummary order);
Task Joined(string channel);
}
public class NotificationHub : Hub<INotificationClient>
{
public async Task SendNotification(string channel, string message)
{
await Clients.Group(channel).ReceiveNotification(message);
}
}
No more magic strings for method names. Refactoring is safe.
Authentication
SignalR supports the same authentication as your ASP.NET Core application. For Azure SignalR Service, the access token is negotiated automatically:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
builder.Services.AddSignalR()
.AddAzureSignalR();
[Authorize]
public class NotificationHub : Hub<INotificationClient>
{
public override Task OnConnectedAsync()
{
var userId = Context.UserIdentifier; // from ClaimTypes.NameIdentifier
return base.OnConnectedAsync();
}
}
The UserIdentifier is extracted from the JWT claim, allowing targeted messages via Clients.User(userId).
Scaling Considerations
The free tier supports 20 concurrent connections — fine for development. The standard tier charges per unit, with each unit supporting 1,000 concurrent connections. Units scale independently of your compute.
This is the key advantage: your App Service or Container App handles HTTP requests and hub method logic, while Azure SignalR Service handles the connection-heavy lifting. You can scale your compute based on CPU and memory, without worrying about connection limits.
For most .NET applications that need real-time features, Azure SignalR Service removes the hardest scaling problem and lets you focus on the application logic.