Updated on
A SignalR client does not reconnect on its own. Calling withAutomaticReconnect() on HubConnectionBuilder opts the connection in, and the client then retries after 0, 2, 10 and 30 seconds before giving up.
Those four attempts are the whole default. Pass an array of millisecond delays to change the timing and the count, or an object implementing IRetryPolicy to decide each delay at the moment it is needed.
We highly recommend reading that article first and trying out the project we created there. That will help you understand the concept of ASP.NET Core SignalR and will make it easy to follow along with this article.
Furthermore, we have learned how to send messages to a specific client in the SignalR: Send a Message to a Specific Client in ASP.NET Core article. However, while implementing a SignalR solution, we often come across an issue of connection getting lost intermittently. Once the SignalR disconnects, we lose the real-time behavior of our application. Then, we need to establish the connection again for the application to function properly. SignalR does not reconnect automatically after a disconnection.
Previously, we had to implement custom mechanisms for the automatic reconnection of SignalR. But that is not a straightforward task. We need to take many things into account while implementing such a solution. The good news is that, in ASP.NET Core 3.0, SignalR introduced the automatic reconnect feature which will help us to solve this problem.
Let’s move on.
Why Does a SignalR Connection Drop?
SignalR prefers WebSockets, which enable two-way communication between the browser and the server. That means it will use WebSockets whenever available, and gracefully fall back to Server-Sent Events or long polling when a WebSocket connection cannot be established. If the difference between the two models is new, our how WebSockets compare with REST article covers it.
A WebSocket stays open until something closes it: a server restart or deployment, an idle timeout on a proxy or load balancer in front of the app, a network change on the client, or the browser suspending an inactive tab.
Let’s take a look at the SignalR app that we created in the previous article:
We can see that once we run the app and keep it idle, the SignalR disconnects after some time. Once it disconnects, we can see an error message in the browser console.
On losing the connection, SignalR does not automatically attempt to reconnect. So we’ll lose the real-time behavior of our app. Then, the only way to re-establish the connection is by refreshing the page. This presents an unpleasant experience for the users.
What Does withAutomaticReconnect() Do?
SignalR does not reconnect on its own. withAutomaticReconnect() on HubConnectionBuilder opts a connection in, and it has to be called explicitly because opting in is not the default.
With no argument, the client waits 0, 2, 10 and 30 seconds before each of four reconnect attempts, then stops. Passing an array of millisecond delays replaces both the timing and the count: the length of the array is how many attempts the client makes.
The connection does not go straight to closed while this happens. It moves to the Reconnecting state and fires onreconnecting, and it reaches onclose only once every attempt has failed.
One case is not covered, and it is the one people assume is. withAutomaticReconnect() does nothing about a failed initial start(). If the first connection attempt fails, the promise rejects, the connection is Disconnected, and no retry is scheduled.
Handling that first failure is our own code, usually a catch on start() that tries again on a timer.
The feature arrived in ASP.NET Core 3.0, and the SignalR JavaScript client has to be version 3.0.0 or above for it to work. Writing the default schedule out by hand looks like withAutomaticReconnect([0, 2000, 10000, 30000]) — four delays, so four attempts.
How Do We Set Custom SignalR Retry Delays?
Two arguments customise the retry schedule, and they answer different questions.
An array of millisecond delays is the simple form. withAutomaticReconnect([0, 0, 10000]) retries immediately, retries immediately again, waits ten seconds for a third attempt, and then stops, because three delays mean three attempts.
An object implementing IRetryPolicy is the general form. Its single method, nextRetryDelayInMilliseconds, receives a RetryContext and returns the milliseconds to wait, or null to stop retrying altogether.
The context is what makes the policy worth writing. It carries previousRetryCount, elapsedMilliseconds and retryReason, so the delay can grow with the attempt number, jitter can be added to stop every client reconnecting in the same instant, and retrying can stop after a wall-clock budget rather than an attempt count.
Backing off matters more than it looks. Every client that dropped is retrying against a server that has just come back, and a fixed short delay turns a recovery into a second outage.
The general form of that reasoning — backoff, jitter and giving up — is the subject of our retry logic in C# article.
Adding Automatic Reconnect to the Hub Connection
We have looked at the automatic reconnect option available in SignalR. Now, without any further ado, let’s implement the same in our project. We are going to modify the project that we created in the linked article.
So, we are going to modify the startConnection() method of the SignalRService class to enable automatic reconnect:
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:5001/chart')
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build();
Here, we call the withAutomaticReconnect() method without any parameters. So this will wait for 0, 2, 10, and 30 seconds respectively before attempting each reconnection. Finally, it will stop after four failed attempts. Of course, we can customize this by passing a set of custom values as the argument.
Furthermore, we have made a call to the configureLogging() method by passing the LogLevel argument as LogLevel.Information. This will configure console logging for the HubConnection instance. The LogLevel.Information argument will enable logging of all events of severity Information or more into the console. We’ve done this so that we can get a log of all events happening in the background.
That’s it. We have configured our SignalR app for automatic reconnection.
Testing The SignalR Automatic Reconnect Feature
Now, let’s test the automatic reconnection behavior of our SignalR app.
For that, we have to run both our server and client app. We can see that the Angular app displays the chart. The server-side application keeps on updating the chart values at regular intervals:
Now, we are going to simulate a WebSocket disconnection scenario. For that, we just need to stop our server application for a few seconds and start it again. We can see that the real-time updates on our Angular app stop for some time when the server application is stopped. But, once our server application is back online, the WebSocket connection will be automatically established again. Cool!
Let’s observe the console to understand the events happening in the background:
Here, we can see that initially the WebSocket connection is established and our client app gets real-time data from the server application.
Then, once we stop the server application, the WebSocket gets disconnected. It immediately makes the first attempt to reconnect, but fails.
After that, it attempts another reconnect in 2 seconds, which fails as well. We can see that the next attempt is made after 10 seconds and by that time, our server application is back online and then the connection becomes successful.
That is the best case, and it is worth knowing what the other one looks like. The gaps grow, so the fourth attempt is scheduled a full thirty seconds after the third one fails, and the client sleeps that whole delay rather than polling. A server that comes back during the last gap therefore leaves the app dead for up to thirty seconds longer than it needs to be.
There is also no fifth attempt. After the fourth one fails the connection closes for good, and only a page refresh or our own code brings it back.
Requesting Real Time Data After Reconnection
At this point, we have enabled our client app to automatically reconnect to our server app using SignalR, but we don’t get real-time data now. The connection is back again, as we can see in our presentation, but the chart is frozen since we don’t get any new data from the server.
To enable the data flow once the connection is alive again, we have to use the onreconnected function that we can call with our hubConnection property inside the Signalr service:
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class SignalrService {
constructor(private http: HttpClient) {}
...
this.hubConnection.onreconnected(() => {
this.http.get('https://localhost:5001/api/chart')
.subscribe(res => {
console.log(res);
})
})
}
The HttpClient import and the constructor injection are what make this.http available inside the service; without them the handler does not compile.
This time, as soon as our app is reconnected, we will receive the data from the server.
Which SignalR Reconnect Events Should We Handle?
Three callbacks cover the whole lifecycle, and the article’s example uses one of them.
onreconnecting runs the moment the connection is lost, before the first retry, and receives the error that closed it. It is where a send button gets disabled and the user gets told the app is offline.
onreconnected runs when a retry succeeds, and it receives a connection id. That id is a new one: the server treats a reconnect as an entirely new connection, so anything stored against the old id is now pointing at nothing.
onclose runs when the retries are exhausted, and at that point the connection is finished. Nothing further is scheduled, so this is where the user is told a refresh is needed.
Refetching state belongs in onreconnected rather than in the handler for incoming data. Whatever the server pushed while the client was retrying is gone, and only an explicit request gets it back.
| API | When it runs | What it hands us | What belongs there |
|---|---|---|---|
onreconnecting(cb) | The connection is lost and retrying is about to start | The Error that closed the connection | Disable anything that sends, and tell the user the app is offline |
onreconnected(cb) | A retry succeeds | A new connectionId — never the old one | Re-enable the UI, replace anything stored against the old id, and re-request state the client missed |
onclose(cb) | The retries are exhausted, or stop() was called | undefined after exhausted retries; the Error only when the connection closed without a retry policy | Tell the user the connection is gone and a refresh is needed |
connection.state | Always readable | Connected, Reconnecting, Disconnected, Connecting or Disconnecting | Guard sends — invoking while Reconnecting rejects |
connection.connectionId | Always readable | The live id, and null for the whole time the client is Reconnecting | Read it, never cache it |
ASP.NET Core 8.0 added a related feature this article predates: stateful reconnect. The client opts in with .withStatefulReconnect() and the server opts in per hub:
app.MapHub<ChartHub>("/chart", options => { options.AllowStatefulReconnects = true; });
With it enabled, both sides buffer and acknowledge messages and replay whatever was in flight, so a blip costs no data, and StatefulReconnectBufferSize caps that buffer (100,000 bytes by default). The boundary matters more than the feature. The buffer lives in the server process, so stateful reconnect survives a transport blip and not a server restart — and a server restart is exactly the scenario we tested above. Measured on .NET 10 with the buffer enabled on both sides, restarting the server still produced a brand-new connection id.
The endpoint our onreconnected handler calls is a controller holding an IHubContext<ChartHub>, and our calling a SignalR hub from a controller article explains what is on the other end of that request. For the same chart against a different front end, see the same real-time chart in Blazor WebAssembly.
Conclusion
In this article we looked at the following topics:
- The disconnection problem of SignalR
- Automatic Reconnect feature of SignalR
- How to implement the automatic reconnection in our SignalR app
Tested with .NET 10.0.10, Angular 13.2 and @microsoft/signalr 6.0.4.




I have tried this and it is working, but I run into an annoying issue, that while hosting this on IIS, after a reconnect Angular stops updating the UI automatically. The events still come in, and any UI interaction like changing a meaningless toggle updates the UI and shows that messages came in. Do you have any idea what the cause can be? We’re a bit stumped on this one.
Hi Arwin. I really don’t have a clue here. I don’t know why would SignalR reconnecting mess up your UI updates. This sounds very strange to me, to be honest.
I agree. But still, whenever a reconnect has happened, the UI only updates if I toggle a button hor hide the menu. I still get the messages, but the UI no longer updates. Did not find an explanation yet.
Nice article this is what I am looking for. Thanks Code-Maze.
You are most welcome Tommy. Glad to hear that.
Thanks!!!!
You are most welcome
Marinko,
I was looking at your solution on Github.
I cant find the message: “HubConnection reconnected successfully”
I need to know where that happend!
Thanks!
This is the message from SignalR, not our own custom message.
Marinko! Thanks for your quick response. I supposed that. I need to send an operation to my api when the reconnectios its done but i can’t find where exactly that event occurs.
Otherwise I need the API to respond to an object when connecting to that hub.
https://github.com/dotnet/aspnetcore/issues/17900
I supose that i no have solution from de ui side.
I’ll investigate from de server side.
Nice article.
Is there any reason you use couple of years old @aspnet/signalr and not new @microsoft/signalr?
Thanks.
As you can read, this is the continuation to our previous article, wich I wrote few years ago. Back than, the @aspnet/signalr was go to library, so in this one we just continued using the same one.
Oh, thanks. I saw this article has been updated recently, so I was worried as I recently upgraded to @microsoft/signalr. 🙂
You are welcome. I think implementation from articles are the same since the aspnet was ported to microsoft library.