SignalR on .NET 10 — Real-Time Communication, Scale-Out, and Notification Push for Production
Posted on: 4/17/2026 9:10:35 PM
Table of contents
- 1. SignalR — The real-time framework for .NET
- 2. Transport protocols — WebSocket, SSE, and Long Polling
- 3. Battle-tested patterns with SignalR
- 4. SignalR client — JavaScript and Vue.js
- 5. Scale-out — from a single server to millions of connections
- 6. SignalR vs SSE vs raw WebSocket — pick the right tool
- 7. Authentication and authorization for SignalR
- 8. Best practices and anti-patterns
- 9. Conclusion
In the modern web, users expect everything to be instant — chat messages appear immediately, dashboards update in real time, notifications push in without a page refresh. SignalR on ASP.NET Core is the production-ready solution for building these real-time features on the .NET platform, with automatic transport selection, strongly-typed hubs, and the ability to scale out to millions of connections. This article takes a deep look at SignalR architecture on .NET 10, battle-tested patterns, scale-out strategies with Redis Backplane and Azure SignalR Service, and comparisons with SSE and raw WebSocket.
1. SignalR — The real-time framework for .NET
ASP.NET Core SignalR is an open-source library that simplifies adding real-time functionality to web applications. Instead of having to manage WebSocket connections, heartbeats, reconnection, and fallback transports yourself, SignalR abstracts all that complexity behind the concept of a Hub — a high-level API that lets the server invoke methods on the client and vice versa.
1.1. The Hub architecture — the communication hub
The Hub is SignalR's core concept. It acts as a bidirectional pipeline between server and client. The server can call methods on any client (or group of clients), and the client can invoke methods on the server — all over a single connection.
graph TB
C1["Client 1
(Browser)"] -->|"WebSocket"| HUB["SignalR Hub
(ASP.NET Core)"]
C2["Client 2
(Mobile)"] -->|"SSE Fallback"| HUB
C3["Client 3
(Desktop)"] -->|"WebSocket"| HUB
HUB -->|"SendAsync"| C1
HUB -->|"SendAsync"| C2
HUB -->|"SendAsync"| C3
HUB --> GROUPS["Groups
(room-123, admin)"]
HUB --> USERS["Users
(userId mapping)"]
GROUPS -->|"Group Broadcast"| C1
GROUPS -->|"Group Broadcast"| C3
style HUB fill:#e94560,stroke:#fff,color:#fff
style C1 fill:#2c3e50,stroke:#e94560,color:#fff
style C2 fill:#2c3e50,stroke:#e94560,color:#fff
style C3 fill:#2c3e50,stroke:#e94560,color:#fff
style GROUPS fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style USERS fill:#f8f9fa,stroke:#e94560,color:#2c3e50
Figure 1: SignalR Hub architecture — server and client communicate bidirectionally over a single connection
// Defining a Hub on the server — .NET 10
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
// Send to ALL connected clients
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName)
.SendAsync("UserJoined", Context.User?.Identity?.Name);
}
public override async Task OnConnectedAsync()
{
// Automatically fires when a client connects
await Clients.Caller.SendAsync("Connected", Context.ConnectionId);
await base.OnConnectedAsync();
}
}
1.2. Strongly-Typed Hubs — Type safety both ways
One of the most powerful features of SignalR on .NET is the Strongly-Typed Hub. Instead of using magic strings like "ReceiveMessage", you define an interface for the client methods — the compiler checks it at build time, so runtime errors from typos disappear.
// Define the contract for client methods
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task UserJoined(string userName);
Task UserLeft(string userName);
Task TypingIndicator(string userName, bool isTyping);
}
// A strongly-typed Hub — the compiler checks every SendAsync call
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
// Clients.All returns IChatClient — full IntelliSense
await Clients.All.ReceiveMessage(user, message);
}
public async Task StartTyping()
{
var userName = Context.User?.Identity?.Name ?? "Anonymous";
await Clients.Others.TypingIndicator(userName, true);
}
}
Why use Strongly-Typed Hubs?
With a strongly-typed Hub, if you rename ReceiveMessage to OnMessageReceived on the interface but forget to update the JavaScript client, the compiler flags the error immediately. This matters most in large projects with many Hub methods — you avoid runtime typo errors that only surface in manual testing.
2. Transport protocols — WebSocket, SSE, and Long Polling
SignalR automatically negotiates the best transport at connection time. The process runs in two steps: (1) the client sends POST /negotiate to fetch the list of transports the server supports, (2) the client picks the best transport that both sides support.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: POST /chat/negotiate
S-->>C: {connectionId, transports: [WS, SSE, LP]}
alt WebSocket available
C->>S: Upgrade: websocket
S-->>C: 101 Switching Protocols
Note over C,S: Full-duplex communication
else SSE fallback
C->>S: GET /chat?transport=SSE
S-->>C: text/event-stream (server→client)
C->>S: POST /chat (client→server)
else Long Polling last resort
C->>S: GET /chat?transport=LongPolling
Note over C,S: Server holds the request until data is available
S-->>C: Response with data
C->>S: GET (poll again)
end
Figure 2: Transport negotiation — SignalR auto-selects the optimal protocol
| Transport | Direction | Latency | Browser support | When to use |
|---|---|---|---|---|
| WebSocket | Full-duplex | Lowest (~1 ms) | Every modern browser | Default — chat, games, collaboration |
| Server-Sent Events | Server → Client | Low | All (except IE) | Dashboards, notifications, progress bars |
| Long Polling | Emulated duplex | High (poll interval) | Every browser | Last-resort fallback (corporate proxies) |
2.1. Server-Sent Events on .NET 10 — Major improvement
.NET 10 adds Results.ServerSentEvents — a new API that wraps IAsyncEnumerable<T> into a standard SSE response without any extra library. Combined with Kestrel's HTTP/2 support, SSE on .NET 10 sidesteps the 6-connections-per-domain limit (HTTP/1.1) — multiple SSE streams can share a single TCP connection via HTTP/2 multiplexing.
// .NET 10 — SSE with Minimal API (no SignalR needed)
app.MapGet("/dashboard/stream", (
DashboardService dashboard,
CancellationToken ct) =>
{
async IAsyncEnumerable<DashboardUpdate> StreamUpdates(
[EnumeratorCancellation] CancellationToken token)
{
while (!token.IsCancellationRequested)
{
yield return await dashboard.GetLatestMetricsAsync(token);
await Task.Delay(TimeSpan.FromSeconds(2), token);
}
}
return Results.ServerSentEvents(StreamUpdates(ct));
});
SignalR or plain SSE?
If your real-time feature only needs one-way server push (dashboard metrics, notification feeds, progress bars), SSE with Results.ServerSentEvents on .NET 10 is the simpler choice — no Hub, no client library, no sticky sessions, no backplane. SignalR wins when you need two-way communication (chat, collaborative editing, game state sync) or complex Groups/Users management.
3. Battle-tested patterns with SignalR
3.1. Chat Room with Groups and presence
This is the classic SignalR use case. Groups let you broadcast to a subset of clients, while presence tracking monitors who's online.
public class ChatHub : Hub<IChatClient>
{
private static readonly ConcurrentDictionary<string, UserInfo> _onlineUsers = new();
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier!;
var userName = Context.User!.FindFirst("name")!.Value;
_onlineUsers.TryAdd(Context.ConnectionId, new UserInfo(userId, userName));
// Broadcast the online user list to the newcomer
await Clients.Caller.OnlineUsers(_onlineUsers.Values.ToList());
await Clients.Others.UserJoined(userName);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (_onlineUsers.TryRemove(Context.ConnectionId, out var user))
await Clients.Others.UserLeft(user.Name);
}
public async Task SendToRoom(string room, string message)
{
var sender = _onlineUsers[Context.ConnectionId];
var chatMessage = new ChatMessage(sender.Name, message, DateTime.UtcNow);
await Clients.Group(room).ReceiveMessage(chatMessage);
}
public async Task JoinRoom(string room)
{
await Groups.AddToGroupAsync(Context.ConnectionId, room);
var sender = _onlineUsers[Context.ConnectionId];
await Clients.Group(room).UserJoined(sender.Name);
}
public async Task LeaveRoom(string room)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, room);
var sender = _onlineUsers[Context.ConnectionId];
await Clients.Group(room).UserLeft(sender.Name);
}
}
3.2. Real-time dashboard with a background service
In many cases, real-time data doesn't originate from the client — it comes from a background process: order processing workers, metric collectors, event consumers. SignalR lets you inject IHubContext into any service to push data to clients.
graph LR
DB["Database
Event Store"] -->|"Change Event"| BG["Background
Service"]
KAFKA["Kafka / Redis
Stream"] -->|"Consume"| BG
BG -->|"IHubContext"| HUB["SignalR Hub"]
HUB -->|"Push Update"| D1["Dashboard 1"]
HUB -->|"Push Update"| D2["Dashboard 2"]
HUB -->|"Push Update"| D3["Mobile App"]
style HUB fill:#e94560,stroke:#fff,color:#fff
style BG fill:#2c3e50,stroke:#e94560,color:#fff
style DB fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style KAFKA fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D1 fill:#2c3e50,stroke:#e94560,color:#fff
style D2 fill:#2c3e50,stroke:#e94560,color:#fff
style D3 fill:#2c3e50,stroke:#e94560,color:#fff
Figure 3: Push data from a background service to dashboard clients via IHubContext
// A background service pushes metrics to dashboards every 2 seconds
public class MetricsPushService : BackgroundService
{
private readonly IHubContext<DashboardHub, IDashboardClient> _hubContext;
private readonly IMetricsCollector _metrics;
public MetricsPushService(
IHubContext<DashboardHub, IDashboardClient> hubContext,
IMetricsCollector metrics)
{
_hubContext = hubContext;
_metrics = metrics;
}
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var snapshot = await _metrics.CollectAsync(ct);
// Push to every client viewing the dashboard
await _hubContext.Clients
.Group("dashboard-viewers")
.MetricsUpdated(new DashboardSnapshot
{
ActiveUsers = snapshot.ActiveUsers,
RequestsPerSecond = snapshot.RPS,
ErrorRate = snapshot.ErrorRate,
P99Latency = snapshot.P99,
Timestamp = DateTime.UtcNow
});
await Task.Delay(TimeSpan.FromSeconds(2), ct);
}
}
}
3.3. Targeted notifications — right person, right time
SignalR supports sending a message to a specific user via Clients.User(userId). The user identifier is mapped automatically from ClaimTypes.NameIdentifier on the JWT token or cookie authentication.
// Push a real-time notification to a user from anywhere in the app
public class OrderService
{
private readonly IHubContext<NotificationHub, INotificationClient> _hub;
public async Task CompleteOrderAsync(Order order)
{
await _repository.UpdateStatusAsync(order.Id, OrderStatus.Completed);
// Real-time push to the user (across all open tabs/devices)
await _hub.Clients.User(order.UserId).OrderStatusChanged(new OrderNotification
{
OrderId = order.Id,
Status = "Completed",
Message = $"Order #{order.Id} is complete!",
Timestamp = DateTime.UtcNow
});
}
}
User vs ConnectionId
Clients.User(userId) sends to all connections belonging to a user (multiple tabs, multiple devices). Clients.Client(connectionId) only sends to one specific connection. For most notification use cases, use User() — a user with the app open on both laptop and phone receives the notification on both.
4. SignalR client — JavaScript and Vue.js
On the client side, SignalR ships the @microsoft/signalr library for JavaScript/TypeScript. Vue.js integration is clean with a composable pattern.
// composables/useSignalR.ts — A Vue 3 composable for SignalR
import { ref, onMounted, onUnmounted } from 'vue'
import * as signalR from '@microsoft/signalr'
export function useSignalR(hubUrl: string) {
const connection = ref<signalR.HubConnection | null>(null)
const isConnected = ref(false)
const start = async () => {
connection.value = new signalR.HubConnectionBuilder()
.withUrl(hubUrl, {
accessTokenFactory: () => localStorage.getItem('token') || ''
})
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.configureLogging(signalR.LogLevel.Warning)
.build()
connection.value.onreconnecting(() => {
isConnected.value = false
})
connection.value.onreconnected(() => {
isConnected.value = true
})
connection.value.onclose(() => {
isConnected.value = false
})
await connection.value.start()
isConnected.value = true
}
const on = (method: string, callback: (...args: any[]) => void) => {
connection.value?.on(method, callback)
}
const invoke = async (method: string, ...args: any[]) => {
return connection.value?.invoke(method, ...args)
}
onMounted(start)
onUnmounted(() => connection.value?.stop())
return { connection, isConnected, on, invoke }
}
<!-- components/ChatRoom.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useSignalR } from '@/composables/useSignalR'
const { on, invoke, isConnected } = useSignalR('/hubs/chat')
const messages = ref<ChatMessage[]>([])
const newMessage = ref('')
const currentRoom = ref('general')
onMounted(() => {
on('ReceiveMessage', (msg: ChatMessage) => {
messages.value.push(msg)
})
on('UserJoined', (userName: string) => {
messages.value.push({
sender: 'System',
content: `${userName} joined the room`,
timestamp: new Date()
})
})
invoke('JoinRoom', currentRoom.value)
})
const sendMessage = async () => {
if (!newMessage.value.trim()) return
await invoke('SendToRoom', currentRoom.value, newMessage.value)
newMessage.value = ''
}
</script>
5. Scale-out — from a single server to millions of connections
By default, SignalR keeps connection state in a single process's memory. When you deploy multiple server instances (horizontal scaling), client A connecting to server 1 can't receive messages from client B connected to server 2. That's the scale-out problem to solve.
5.1. Redis Backplane — self-managed scale-out
The Redis Backplane uses Redis Pub/Sub to synchronize messages between server instances. When server 1 needs to broadcast a message, it publishes to a Redis channel; all other servers subscribed to that channel forward the message to their clients.
graph TB
C1["Client A"] --> S1["Server 1"]
C2["Client B"] --> S1
C3["Client C"] --> S2["Server 2"]
C4["Client D"] --> S2
C5["Client E"] --> S3["Server 3"]
S1 <-->|"Pub/Sub"| REDIS["Redis
Backplane"]
S2 <-->|"Pub/Sub"| REDIS
S3 <-->|"Pub/Sub"| REDIS
LB["Load Balancer
(Sticky Sessions)"] --> S1
LB --> S2
LB --> S3
style REDIS fill:#e94560,stroke:#fff,color:#fff
style LB fill:#2c3e50,stroke:#e94560,color:#fff
style S1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style S2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style S3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C1 fill:#2c3e50,stroke:#e94560,color:#fff
style C2 fill:#2c3e50,stroke:#e94560,color:#fff
style C3 fill:#2c3e50,stroke:#e94560,color:#fff
style C4 fill:#2c3e50,stroke:#e94560,color:#fff
style C5 fill:#2c3e50,stroke:#e94560,color:#fff
Figure 4: Redis Backplane — synchronizes messages between multiple server instances via Pub/Sub
// Program.cs — Redis Backplane setup
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration = builder.Configuration
.GetConnectionString("Redis")!;
options.Configuration.ChannelPrefix =
RedisChannel.Literal("ChatApp");
});
var app = builder.Build();
app.MapHub<ChatHub>("/hubs/chat");
app.Run();
The Redis Backplane still needs sticky sessions!
SignalR uses a two-step negotiate flow: (1) the client calls /negotiate to fetch a connectionId, (2) the client opens the transport (WebSocket/SSE) with that connectionId. Both requests must go to the same server — otherwise the second server can't find the connection context. Configuring your load balancer with sticky sessions (by cookie or IP) is mandatory when using the Redis Backplane.
5.2. Azure SignalR Service — managed scale-out
If you deploy on Azure and want to avoid managing a Redis backplane and sticky sessions, Azure SignalR Service is the managed option. Azure manages every WebSocket connection — your app server only runs business logic and doesn't hold connection state.
graph LR
C1["Clients"] -->|"WebSocket"| AZURE["Azure SignalR
Service"]
AZURE -->|"Server Connection"| S1["App Server 1"]
AZURE -->|"Server Connection"| S2["App Server 2"]
S1 --> DB["Database"]
S2 --> DB
style AZURE fill:#e94560,stroke:#fff,color:#fff
style S1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style S2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C1 fill:#2c3e50,stroke:#e94560,color:#fff
style DB fill:#f8f9fa,stroke:#e94560,color:#2c3e50
Figure 5: Azure SignalR Service — clients connect to Azure, app servers only handle logic
// Program.cs — Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString = builder.Configuration
.GetConnectionString("AzureSignalR")!;
options.ServerStickyMode =
ServerStickyMode.Required; // Ensures consistency
});
| Criterion | Redis Backplane | Azure SignalR Service |
|---|---|---|
| Sticky sessions | Required | Not required |
| Connection management | App server holds it | Azure manages it |
| Scale limit | Depends on Redis + servers | Millions of connections |
| Latency | Low (same datacenter) | Extra hop to Azure |
| Cost | Cost of Redis servers | Per-unit pricing (~$50/unit) |
| Message loss if Redis goes down | Yes (no buffer) | Azure guarantees delivery |
| Pick when | Self-hosted, latency-critical | Azure-hosted, need scale >10K connections |
6. SignalR vs SSE vs raw WebSocket — pick the right tool
Not every real-time feature needs SignalR. Here's a decision framework based on the actual requirements:
| Requirement | Solution | Why |
|---|---|---|
| One-way server push (dashboards, notifications) | SSE + Results.ServerSentEvents | No client library, no backplane, HTTP/2 multiplexing |
| Chat, collaborative editing, games | SignalR | Two-way communication, Groups, Users, auto-reconnect, type safety |
| Custom binary protocol, video/audio streaming | Raw WebSocket | Full control over framing, no SignalR protocol overhead |
| IoT sensor data (thousands of devices) | MQTT or raw WebSocket | SignalR's overhead doesn't fit IoT scale |
| Need to scale >100K connections on Azure | Azure SignalR Service | Managed, no sticky sessions, auto-scale |
General principle for 2026
Most "real-time" features in enterprise apps only need unidirectional push — dashboard metrics, order status, notification feeds. In those cases, SSE with Results.ServerSentEvents on .NET 10 is the simplest pick with the least infrastructure overhead. Reach for SignalR when you genuinely need bidirectional communication or advanced features like Groups, Presence, and Strongly-Typed Hubs.
7. Authentication and authorization for SignalR
SignalR Hubs can use the same authentication/authorization pipeline as ASP.NET Core. With JWT Bearer tokens, clients pass the token via query string (since browsers can't set custom headers on a WebSocket connection).
// Program.cs — Authentication for SignalR Hubs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// SignalR passes the token via query string
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;
}
};
});
// Hub with authorization
[Authorize]
public class SecureChatHub : Hub<IChatClient>
{
[Authorize(Roles = "Admin")]
public async Task DeleteMessage(string messageId)
{
await Clients.All.MessageDeleted(messageId);
}
public async Task SendMessage(string message)
{
var userId = Context.UserIdentifier!;
var userName = Context.User!.FindFirst("name")!.Value;
await Clients.Others.ReceiveMessage(
new ChatMessage(userName, message, DateTime.UtcNow));
}
}
8. Best practices and anti-patterns
8.1. Managing the connection lifetime
// Client-side: automatic reconnect with exponential backoff
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/chat")
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
// Exponential backoff: 0s, 2s, 4s, 8s, 16s, max 30s
const delay = Math.min(
Math.pow(2, retryContext.previousRetryCount) * 1000,
30000
);
return delay;
}
})
.build();
// Handle reconnection events
connection.onreconnecting((error) => {
showToast("Reconnecting...", "warning");
});
connection.onreconnected((connectionId) => {
showToast("Reconnected!", "success");
// Re-join rooms after reconnecting
connection.invoke("JoinRoom", currentRoom);
});
connection.onclose((error) => {
showToast("Connection lost. Please reload the page.", "error");
});
8.2. Anti-patterns to avoid
| Anti-pattern | Problem | Fix |
|---|---|---|
| Sending large payloads through the Hub | Blocks the connection, increases memory pressure | Upload files via HTTP; only send notifications via SignalR |
| Blocking calls inside a Hub method | Blocks the I/O thread, reduces throughput | Always use async/await — never Task.Result |
| Storing state in a static field of the Hub | Hub instances are created per invocation | Use a DI service (Singleton/Scoped) or IHubContext |
| Not handling reconnection | Users don't realize they lost the connection | withAutomaticReconnect() + UI feedback |
| Broadcasting too often | Floods clients, bloats bandwidth | Throttle/debounce on the server; send only deltas |
Hub instances are transient!
Every time a client invokes a Hub method, ASP.NET Core creates a new Hub instance. Don't store state in Hub class properties or fields — it'll be lost after each invocation. Instead, use IHubContext injected into a Singleton service, or use external storage (Redis, database) for shared state.
8.3. Performance tuning
// Program.cs — Tuning SignalR for production
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = false; // Production: disable detailed errors
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.HandshakeTimeout = TimeSpan.FromSeconds(5);
options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB max message
options.StreamBufferCapacity = 10;
})
.AddMessagePackProtocol(); // Binary protocol instead of JSON — ~30% smaller
The MessagePack protocol
By default, SignalR uses a JSON protocol. Switching to MessagePack (binary serialization) shrinks messages by ~30% and accelerates serialization/deserialization. Especially effective for numeric data (dashboard metrics, game state). Install with: dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack.
9. Conclusion
SignalR on .NET 10 remains the strongest production-ready real-time solution in the .NET ecosystem. With Strongly-Typed Hubs, automatic transport negotiation, and scale-out via Redis Backplane or Azure SignalR Service, it fully addresses real-time communication — from simple chat to million-connection dashboards.
That said, .NET 10 also brings Results.ServerSentEvents — a considerably lighter option when all you need is one-way server push. The rule of thumb: SSE for unidirectional push (dashboards, notification feeds), SignalR for bidirectional communication (chat, collaboration), and raw WebSocket for custom binary protocols. Don't default to SignalR for every real-time feature — picking the right tool cuts complexity and infrastructure cost significantly.
References
- Introduction to ASP.NET Core SignalR — Microsoft Learn
- Redis backplane for ASP.NET Core SignalR scale-out — Microsoft Learn
- ASP.NET Core SignalR production hosting and scaling — Microsoft Learn
- Performance best practices with gRPC — Microsoft Learn
- SSE vs SignalR vs WebSockets in ASP.NET Core (2026) — Coding Droplets
- Scaling SignalR With a Redis Backplane — Milan Jovanović
- Scaling SignalR: Scaleout strategies, limits & alternatives — Ably
Cloudflare AI Platform 2026 — Edge Infrastructure for Serverless AI Agents
Cloudflare Developer Platform 2026 — A Free Edge Computing Ecosystem for Developers
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.