#4736·SignalR

Issue: Missing Heartbeats in SignalR Client Despite Active Connection

Author: rmadaneCreated Feb 24, 2025Updated Jan 13, 2026

Description I am experiencing an issue where my SignalR client occasionally misses sending heartbeats to the server, even though the connection remains active. The expected behavior is that the client should continuously send heartbeats every 10 seconds, but I can see gaps in the WebSocket message logs where no heartbeat is sent.

Environment Details

  • Client Framework: Angular 16
  • Client Library: @microsoft/signalr (latest)
  • Server Framework: ASP.NET Core 7
  • Hosting Environment: Local development (http://localhost:5292)
  • Transport Mode: WebSockets

Observed Behavior

  • SignalR connection is successfully established.
  • Heartbeats are sent at 10-second intervals initially.
  • Occasionally, heartbeats stop for some time, even when the connection state remains Connected.
  • After reconnection (either automatic or manual), heartbeats sometimes resume, but not always.

Expected Behavior The SignalR client should send heartbeats consistently at the configured interval, even after reconnections.

Client-Side Code (Angular TypeScript)

typescript
import { Component } from '@angular/core';
import { HubConnectionBuilder, HubConnection, HubConnectionState, LogLevel } from '@microsoft/signalr';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
})
export class AppComponent {
  private hubConnection: HubConnection | undefined;
  public tenantId: string = '';
  public agentId: string = '';
  heartbeatPoller: any;

  public connectAgent(): void {
    this.startConnection();
  }

  public disconnectAgent(): void {
    if (this.hubConnection) {
      this.hubConnection.stop();
      this.agentId = '';
      this.tenantId = '';
      clearInterval(this.heartbeatPoller);
    }
  }

  private startConnection() {
    let hubUrl = 'http://localhost:5292/chathub';
    this.hubConnection = new HubConnectionBuilder()
      .withUrl(hubUrl)
      .withAutomaticReconnect()
      .configureLogging(LogLevel.Information)
      .build();

    this.hubConnection.start()
      .then(() => {
        console.log('✅ SignalR connection established.');
        this.startHeartbeat();
      })
      .catch(err => console.error('❌ Error while starting SignalR connection:', err));

    this.hubConnection.onreconnecting(() => {
      console.warn('⚠️ SignalR connection lost. Reconnecting...');
      clearInterval(this.heartbeatPoller);
    });

    this.hubConnection.onreconnected(() => {
      console.log(' Reconnected to SignalR. Restarting heartbeat...');
      this.startHeartbeat();
    });

    this.hubConnection.onclose(() => {
      console.error('❌ SignalR connection closed.');
      clearInterval(this.heartbeatPoller);
      setTimeout(() => this.startConnection(), 5000);
    });
  }

  private startHeartbeat() {
    clearInterval(this.heartbeatPoller);
    this.heartbeatPoller = setInterval(() => {
      this.sendCustomHeartbeat();
    }, 10000);
  }

  private sendCustomHeartbeat(): void {
    if (!this.hubConnection || this.hubConnection.state !== HubConnectionState.Connected) {
      console.warn('⚠️ Skipping heartbeat: Not connected.');
      return;
    }

    let usableIdentifier = `${this.tenantId}:${this.agentId}`;
    this.hubConnection.invoke('HeartbeatFromClient', usableIdentifier)
      .then(() => console.log('✅ Heartbeat sent:', usableIdentifier))
      .catch(err => console.error('❌ Error while sending heartbeat:', err));
  }
}

Server-Side Code (ASP.NET Core)

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowLocalhost", policy =>
    {
        policy.WithOrigins("http://localhost:4200")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

builder.Services.AddSignalR(opts =>
{
    opts.KeepAliveInterval = TimeSpan.FromSeconds(5);
    opts.ClientTimeoutInterval = TimeSpan.FromSeconds(90);
    opts.EnableDetailedErrors = true;
});

var app = builder.Build();

app.UseCors("AllowLocalhost");
app.MapHub<ChatHub>("/chathub").RequireCors("AllowLocalhost");
app.Run();

Troubleshooting Attempts

  • Replaced setTimeout with setInterval in the heartbeat function to ensure consistent execution.
  • Added logging to track missed heartbeats and verify connection state (Connected).
  • Enabled LogLevel.Information to capture detailed logs in the client.
  • Cleared heartbeat intervals on reconnect events to avoid duplicate timers.
  • Increased KeepAliveInterval on the server to prevent early disconnections.

Questions

  • Is there any known issue where SignalR clients occasionally stop sending messages despite remaining connected?
  • Is withAutomaticReconnect() sufficient to handle silent disconnects, or should I manually restart the connection?
  • Should I configure KeepAliveInterval differently for better reliability?

Screenshots

Image