Real-Time Web Applications with Azure SignalR Service
Real-time communication is essential for modern web applications. Whether you’re building a chat application, live dashboard, or collaborative tool, Azure SignalR Service provides a fully managed service that simplifies adding real-time functionality to your applications.
What is Azure SignalR Service?
Azure SignalR Service is a fully managed real-time messaging service that allows you to:
- Push content to connected clients instantly
- Handle millions of concurrent connections
- Scale automatically without managing infrastructure
- Support multiple transports (WebSockets, Server-Sent Events, Long Polling)
Creating Azure SignalR Service
# Create SignalR Service
az signalr create \
--name mysignalr \
--resource-group myResourceGroup \
--location eastus \
--sku Standard_S1 \
--unit-count 1 \
--service-mode Default
# Get connection string
az signalr key list \
--name mysignalr \
--resource-group myResourceGroup \
--query primaryConnectionString \
--output tsv
Building a Chat Application
Server-Side (.NET)
Create a new ASP.NET Core application:
dotnet new web -n SignalRChat
cd SignalRChat
dotnet add package Microsoft.Azure.SignalR
Create the Hub:
// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("UserJoined",
$"User {Context.ConnectionId} joined {groupName}");
}
public async Task SendToGroup(string groupName, string user, string message)
{
await Clients.Group(groupName).SendAsync("ReceiveMessage", user, message);
}
public async Task SendPrivateMessage(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("ReceivePrivateMessage", message);
}
public override async Task OnConnectedAsync()
{
await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
Configure the application:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(builder.Configuration["Azure:SignalR:ConnectionString"]);
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors();
app.MapHub<ChatHub>("/chatHub");
app.Run();
Client-Side (JavaScript)
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>SignalR Chat</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/5.0.4/signalr.min.js"></script>
</head>
<body>
<div id="chat">
<input type="text" id="userInput" placeholder="Your name" />
<input type="text" id="messageInput" placeholder="Message" />
<button onclick="sendMessage()">Send</button>
<ul id="messagesList"></ul>
</div>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on("ReceiveMessage", (user, message) => {
const li = document.createElement("li");
li.textContent = `${user}: ${message}`;
document.getElementById("messagesList").appendChild(li);
});
connection.on("UserConnected", (connectionId) => {
console.log(`User connected: ${connectionId}`);
});
connection.on("UserDisconnected", (connectionId) => {
console.log(`User disconnected: ${connectionId}`);
});
connection.onreconnecting(error => {
console.log(`Connection lost. Reconnecting: ${error}`);
});
connection.onreconnected(connectionId => {
console.log(`Reconnected with ID: ${connectionId}`);
});
async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
}
async function sendMessage() {
const user = document.getElementById("userInput").value;
const message = document.getElementById("messageInput").value;
try {
await connection.invoke("SendMessage", user, message);
document.getElementById("messageInput").value = "";
} catch (err) {
console.error(err);
}
}
start();
</script>
</body>
</html>
Serverless Mode with Azure Functions
For serverless scenarios, use SignalR Service in Serverless mode:
az signalr update \
--name mysignalr \
--resource-group myResourceGroup \
--service-mode Serverless
Azure Function bindings:
// NegotiateFunction.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
public class NegotiateFunction
{
[Function("negotiate")]
public static SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
[SignalRConnectionInfoInput(HubName = "chat")] SignalRConnectionInfo connectionInfo)
{
return connectionInfo;
}
}
// BroadcastFunction.cs
public class BroadcastFunction
{
[Function("broadcast")]
[SignalROutput(HubName = "chat")]
public static SignalRMessageAction Broadcast(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
[FromBody] ChatMessage message)
{
return new SignalRMessageAction("ReceiveMessage")
{
Arguments = new object[] { message.User, message.Text }
};
}
}
// SendToUserFunction.cs
public class SendToUserFunction
{
[Function("sendToUser")]
[SignalROutput(HubName = "chat")]
public static SignalRMessageAction SendToUser(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
[FromBody] DirectMessage message)
{
return new SignalRMessageAction("ReceivePrivateMessage")
{
UserId = message.TargetUserId,
Arguments = new object[] { message.Text }
};
}
}
public record ChatMessage(string User, string Text);
public record DirectMessage(string TargetUserId, string Text);
Live Dashboard Example
Creating a real-time stock ticker:
// Hubs/StockHub.cs
public class StockHub : Hub
{
public async Task SubscribeToStock(string symbol)
{
await Groups.AddToGroupAsync(Context.ConnectionId, symbol);
}
public async Task UnsubscribeFromStock(string symbol)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, symbol);
}
}
// Services/StockService.cs
public class StockService : BackgroundService
{
private readonly IHubContext<StockHub> _hubContext;
private readonly Random _random = new();
public StockService(IHubContext<StockHub> hubContext)
{
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var stocks = new Dictionary<string, decimal>
{
["MSFT"] = 250.00m,
["AAPL"] = 150.00m,
["GOOGL"] = 2000.00m
};
while (!stoppingToken.IsCancellationRequested)
{
foreach (var stock in stocks.Keys.ToList())
{
var change = (decimal)(_random.NextDouble() - 0.5) * 2;
stocks[stock] += change;
await _hubContext.Clients.Group(stock).SendAsync(
"StockUpdate",
new
{
Symbol = stock,
Price = stocks[stock],
Change = change,
Timestamp = DateTime.UtcNow
},
stoppingToken);
}
await Task.Delay(1000, stoppingToken);
}
}
}
Client-side dashboard:
// stock-dashboard.js
const connection = new signalR.HubConnectionBuilder()
.withUrl("/stockHub")
.withAutomaticReconnect()
.build();
connection.on("StockUpdate", (data) => {
updateStockDisplay(data);
});
async function subscribeToStock(symbol) {
await connection.invoke("SubscribeToStock", symbol);
}
async function unsubscribeFromStock(symbol) {
await connection.invoke("UnsubscribeFromStock", symbol);
}
function updateStockDisplay(data) {
const element = document.getElementById(`stock-${data.symbol}`);
if (element) {
element.querySelector('.price').textContent = `$${data.price.toFixed(2)}`;
const changeEl = element.querySelector('.change');
changeEl.textContent = `${data.change >= 0 ? '+' : ''}${data.change.toFixed(2)}`;
changeEl.className = `change ${data.change >= 0 ? 'positive' : 'negative'}`;
}
}
connection.start().then(() => {
subscribeToStock('MSFT');
subscribeToStock('AAPL');
subscribeToStock('GOOGL');
});
Authentication Integration
// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/chatHub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
// Use in Hub
[Authorize]
public class SecureChatHub : Hub
{
public async Task SendMessage(string message)
{
var user = Context.User?.Identity?.Name ?? "Anonymous";
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Conclusion
Azure SignalR Service removes the complexity of managing real-time connections at scale. Whether you’re building chat applications, live dashboards, or collaborative tools, SignalR provides a robust foundation with:
- Automatic connection management
- Multiple transport fallbacks
- Built-in reconnection handling
- Easy integration with Azure Functions for serverless scenarios
The service handles millions of connections, letting you focus on building great real-time experiences.