Architecting Resilient WebSocket Services in ASP.NET Core 8+


Everywhere these days is dominated by real time user experiences, collaborative editing, gaming, live analytics, and financial dashboards. WebSockets have become essential for persistent, low latency communication. While ASP.NET Core has supported WebSockets since version 2.1, .NET 8+ introduces modern and cleaner abstractions for building scalable WebSocket services.
When to Choose WebSockets Over HTTP/2, SignalR, or gRPC
Despite SignalR’s abstraction over WebSockets (and fallback to long polling), direct WebSocket implementation still has a place when:
You want full control over protocol framing and message types.
You need to minimise latency and remove SignalR’s abstraction layer.
You're building binary first protocols or interoperating with non .NET clients.
You don’t want the baggage of negotiation and dependency injection from SignalR.
WebSocket Middleware in .NET 8+
In .NET 8, WebSocketMiddleware
is still part of Microsoft.AspNetCore.WebSockets
, but can now be used more cleanly with top-level routing. Here's a skeleton of a custom WebSocket endpoint using minimal APIs:
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30),
AllowedOrigins = ["https://yourapp.com"]
});
app.Map("/ws", async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
using WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
var socketHandler = context.RequestServices.GetRequiredService<IWebSocketHandler>();
await socketHandler.HandleAsync(webSocket, context);
}
else
{
context.Response.StatusCode = 400;
}
});
Injecting a handler here avoids cluttering your middleware with protocol logic.
Building the IWebSocketHandler
You should never leave your socket lifecycle logic tightly coupled with the middleware so use an interface.
public interface IWebSocketHandler
{
Task HandleAsync(WebSocket socket, HttpContext context);
}
In a concrete implementation, you can structure this using a full-duplex read/write model with a CancellationTokenSource
to track disconnections.
Lifecycle Management & Reconnection
WebSocket disconnections aren’t always obvious, especially in idle or low traffic sessions. To help detect broken connections, implement ping pong logic. This typically involves periodically sending a "ping"
message and expecting a corresponding "pong"
response:
private async Task KeepAliveAsync(WebSocket socket, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(15), token);
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(
Encoding.UTF8.GetBytes("ping"),
WebSocketMessageType.Text,
true,
token
);
}
}
}
On the client side, always implement some kind of retry policy like exponential backoff with jitter to avoid DDOS like reconnections in multi client environments.
Message Framing & Protocol Versioning
Define a clear and consistent message contract with versioning to support forward compatibility. For example:
{
"version": 1,
"type": "CHAT_MESSAGE",
"payload": {
"userId": "abc123",
"message": "Hello, world!"
}
}
Use System.Text.Json
with JsonSerializerOptions.PropertyNameCaseInsensitive = true
for added flexibility in handling client input. Avoid introducing breaking schema changes mid session, Its a good idea to use version negotiation or feature toggles to phase in changes without disrupting live connections.
Scaling with Cloud Native Awareness
WebSockets in Azure:
WebSockets remain a critical transport layer for building responsive, bidirectional communication between clients and servers. When deploying these systems to Azure, it’s important to understand the nuances of WebSocket support across different hosting models to ensure your application remains stable and scalable under load. Azure Web Apps, Azure Container Apps, and Azure Kubernetes Service (AKS) all support WebSockets, allowing you to establish persistent connections with clients. Not all Azure front end services are the same when it comes to handling WebSocket traffic. As of 2025, Azure Front Door doesnt support WebSockets. This limitation rules it out as a viable entry point for WebSocket based applications and requires an alternative routing layer in front of your application. Azure Application Gateway, on the other hand, does support WebSocket pass through. It can successfully proxy WebSocket connections to your backend services, but the caveat is, you must enable and maintain backend session affinity, also known as sticky sessions. This ensures that once a WebSocket connection is established with a particular backend instance, all subsequent communication for that connection is routed to the same server instance which is critical for maintaining the integrity of the socket session. Azure Container Apps also offer native support for WebSockets and include sticky session handling. This makes them a practical option for smaller scale or tightly controlled workloads. Its important to remember though, when scaling out to multiple instances, challenges arise. Since WebSockets are inherently stateful, spreading connections across many replicas without intelligent connection routing can lead to dropped sessions or state inconsistencies. For this reason, Container Apps are best suited to single instance designs or deployments where you’ve implemented custom routing logic that guarantees consistent connection affinity.
For architectures that demand horizontal scale and high availability, managing connection state becomes more complex. WebSockets are not stateless by nature, so storing connection metadata, message queues, or pub/sub coordination in memory is not sustainable across multiple nodes. In these scenarios, it’s essential to handle your connection state differently. Redis Pub/Sub is a popular option for this, where each connection group or topic can be mapped to a Redis stream or channel, allowing multiple instances to publish and subscribe to messages across a distributed system. Another option is Azure SignalR, which remains a good choice even if you decide not to use SignalR's high level APIs on the server. It provides robust connection management, scaling, and client routing features, and can act as a managed WebSocket gateway for your backend, abstracting away the complexity of connection tracking and delivery.
Understanding these infrastructure level concerns is essential when building real time systems in Azure. WebSockets provide the transport, but it’s your architecture that ensures reliability, consistency, and scalability at runtime.
Observability and Failure Detection
Plug in structured logging and metrics for all lifecycle events:
_logger.LogInformation("Client connected from {IpAddress}", context.Connection.RemoteIpAddress);
try
{
// Handle socket...
}
catch (WebSocketException ex)
{
_logger.LogWarning(ex, "WebSocket error from client {IpAddress}", context.Connection.RemoteIpAddress);
}
Integrate with OpenTelemetry to trace messages end-to-end if you're relaying data to backend services.
Clean Cancellation & Graceful Shutdown
On app shutdown or WebSocket drop, clean up lingering connections:
public async Task StopAsync(CancellationToken cancellationToken)
{
foreach (var socket in _connections.Values)
{
if (socket.State == WebSocketState.Open)
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server shutting down", cancellationToken);
}
}
Make this part of your hosted service lifecycle via IHostedService
.
WebSockets are not designed to be a simple plug and play solution at scale. While they provide powerful real time communication capabilities, implementing them in a production grade environment requires deliberate architectural planning. You must account for connection lifecycle management, protocol design consistency, heartbeat mechanisms, client reconnection strategies, and observability. These elements are often overlooked in smaller projects, but they quickly become critical as your system grows or operates under high concurrency. With .NET 8+, the platform offers improved middleware, making it easier to build WebSocket based services that are clean, testable, and maintainable. That said, the framework does not abstract away the inherent complexity of stateful, long lived connections. You are still responsible for handling failure modes, scaling connection aware services across instances, and integrating with external systems such as Redis or SignalR when your architecture demands more than in memory tracking can offer.
If your application requires complete control over the transport layer, low latency communication, and deterministic real time flows, whether for financial data, multiplayer games, live collaboration, or telemetry pipelines, WebSockets remain a highly capable and relevant choice. When combined with modern .NET patterns such as minimal APIs, background services, resilient HTTP clients, and distributed caching, and when deployed within a cloud native infrastructure that supports graceful failover and session aware routing, they can form the backbone of a solid system.
Subscribe to my newsletter
Read articles from Patrick Kearns directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
