I spent some time investigating Azure SignalR Service. I have an existing ASP.NET Core MVC application that uses SignalR, hosted on an Azure App Service. I’m interested in seeing whether I can move the web client to Azure Storage and replace the App Service with a SignalR Service.

What is it?

SignalR is a .NET library that simplifies adding real-time web functionality to apps. Real-time web functionality means we can write server code that pushes content to clients instantly - without those clients having to poll the server or submit new HTTP requests to receive this content. Other solutions that offer similar functionality include socket.io and firehose.io The latest version of SignalR at time of writing is ASP.NET Core SignalR 2.2. SignalR is free and open source!

Azure SignalR Service is a managed Azure offering which handles the responsibility of hosting, scaling and load balancing SignalR applications. SignalR Service is useful where you need to scale, go serverless, or both, with your SignalR application.

What does it cost?

At the time of writing, two plans are available. Prices are for the Australia East region and are in Australian Dollars (AUD).

  FREE STANDARD
Concurrent Connections per Unit 20 1,000
Messages / Unit / Day 20,000 1,000,000
Price / Unit / Day Free $2.2106
Price / Unit / Month Free $67.57
Max Units 1 100

Additional messages for the Standard unit: $1.373/million

Connection counts

Connections are split into server connection and client connections. The total connections is server + client connections.

Server connections

Each SignalR hub raises 5 server connections by default, and each application server you run will result in another hub along with its 5 connections. So, if you have 3 hubs and 2 application servers, then server connections alone will count for 2 * 3 * 5 = 30 concurrent connections prior to any clients connecting. In my case I only have one hub and it’s running on a single App Service instance, so I have 5 server connections.

The default can be changed to whatever value you like, and performance can improved on larger client counts by increasing this number. For example, if you have 100,000 clients in total, the connection count can be increased to 10 or 15 for better throughput.

Client connections

Each client connection then counts as one connection, so if I have 5 clients connected, my total connections is 10 - five (default) from the server, and one from each client.

Message counts

Internal pings are not counted, but outgoing messages are. Any message larger than 2KB counts as Total KB size / 2KB messages, so a 10KB message counts as 5 messages. This means that if you have 5 clients, and one of them sends a 10KB message to the server, followed by the server broadcasting that same message to all five clients, the message count is (10 / 2) + (10 / 2) * 5 = 30.

Does it work?

Switching an existing ASP.NET Core SignalR application to use SignalR Service is trivial. You add the Microsoft.Azure.SignalR package, set a connection string, and append .AddAzureSignalR() to your existing services.AddSignalR() so it becomes services.AddSignalR().AddAzureSignalR();. Routes are configured using UseAzureSignalR instead of UseSignalR.

A recent change causes the following warning to appear. For now I’m ignoring it as an issue has already been raised.

2 endpoints to https://my-url.service.signalr.net found, use the one https://my-url.service.signalr.net

With only minimal changes required, a migration to SignalR Service was rather painless.

Serverless

Going serverless, one can drop the application server requirement (I’m using ASP.NET MVC Core) and instead use Azure Functions. Whilst this is possible, it’s not trivial to move an existing application serverless due to the current limitations of the feature;

  1. Clients are listen only - you cannot send messages from client to the service. Although the SignalR SDK allows client applications to invoke backend logic in a SignalR hub, this functionality is not yet supported when you use SignalR Service with Azure Functions. Use HTTP requests to invoke Azure Functions.
  2. As such, your existing hubs will need to be split up across functions and cannot be used as-is
  3. Now that the hubs are gone, any state that they carried in memory will need to be moved to some other storage

Load testing

ASP.NET SignalR had Crank, ASP.NET Core SignalR has a port called Crankier but neither are documented. Never-the-less, the following tweet seems to be the last time it was benchmarked by the devs.

Other options to load test it could include writing your own app to open a number of client connections and send specific messages, or using tools that support websocket load testing like gatling or Apache JMeter with a websocket plugin.

Conclusion

At this time it doesn’t make sense to move my existing application to SignalR Service. I don’t need the scale as yet, and I cannot drop the application server and go serverless without moving the state currently carried by SignalR hubs within the application into another storage mechanism like Redis or a database. Until I need scale, which for me would likely be around 10,000 concurrent connections, it makes sense to stay with SignalR inside an App Service.

Another interesting finding was that my message count on some features of my existing app would need optimisation. With two clients connected and interacting with a multi-player pong-like game, I found I was sending ~1,000 messages a minute. This didn’t make sense at first when I viewed the metrics, but was clear once I did some calculations. Clients on that specific feature have two buttons, up & down, and a message is sent on both press and release for either button. Upon receiving a button up, or button down message from a client, the server would then send another message reflecting the current state to the admin client. This results in four messages being sent with every button press & release, and spamming presses on both clients quickly smashed the message count. This highlighted that I need to add some kind of debounce and/or spam protection on the existing application.