Course contentsShow
← C#
Lesson 1451 of 1,90813. Real-time Communication with SignalRFree lesson

Implementing Connection Heartbeats

Monitor link health to detect dead connections and trigger automatic reconciliation.

In this lesson, you will learn how to implement connection heartbeats to detect "half-open" connections and proactively trigger reconciliation in SignalR.

The Problem of Ghost Connections

In persistent real-time connections, a client might drop off the network unexpectedly—due to a tunnel collapse, a device sleep mode, or a server-side crash—without formally invoking the OnDisconnectedAsync lifecycle event. These are "ghost" connections. They occupy memory, hold onto hub state, and prevent clean resource cleanup. To fix this, we implement a heartbeat mechanism that acts as a pulse, verifying that the channel is still truly alive.

Implementing the Heartbeat Pulse

A heartbeat is essentially a low-overhead "ping" sent from the server or client at regular intervals. If a client fails to acknowledge a heartbeat (or if the server detects a timeout in the underlying socket), the infrastructure triggers a reconciliation flow.

In SignalR, the framework handles the low-level keep-alive, but you must ensure your middleware pipeline is configured to react to those timeouts. You define the heartbeat interval in your AddSignalR configuration:

builder.Services.AddSignalR(options =>
{
    // The frequency at which the server sends keep-alive frames
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    
    // The time the server waits for a response before terminating the connection
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
});

Triggering Reconciliation

Once a connection is identified as dead, you must handle the cleanup logic. Since the connection state is lost, "reconciliation" often involves resetting the user's specific Hub state or notifying other clients that the user has gone offline. You achieve this by overriding OnDisconnectedAsync in your Hub class:

public override async Task OnDisconnectedAsync(Exception? exception)
{
    // Reconciliation logic: Cleanup session-specific resources
    await _userPresenceService.RemoveUserAsync(Context.ConnectionId);
    
    await base.OnDisconnectedAsync(exception);
}

When the ClientTimeoutInterval expires, SignalR automatically triggers this method, allowing you to reconcile your application state with the reality of the dropped connection. By tuning these intervals, you balance the trade-off between detecting dead links quickly and avoiding false positives caused by temporary network latency.

Key Takeaway: Connection heartbeats replace the ambiguity of TCP timeouts with an explicit keep-alive contract, ensuring that your application state remains synchronized with the actual lifecycle of active client connections.