In this post, I compare two cloud pubsub services: Azure Web PubSub and Ably, to determine which provides the best development experience. The context will be a multi-user pixelart drawing application that I've built with both services.
- How easy is it to use their SDKs/APIs?
- How much code do you need to write to add realtime capabilities to your app?
This is not a step-by-step instruction on how to build the entire application. I'll highlight some key features and differences. The GitHub repository for the app has further details and includes CodeTours to guide you through the solution.
Realtime messaging is taking over the world. Our food delivery apps show us our meal is only 5 minutes away. We use chat applications every day for work and private use. We receive the latest news on sports, stocks and celebrity news on our mobile devices, and we collaborate on digital documents.
As I mentioned in my recent article about pubsub with C#.NET, there are plenty of benefits of using pubsub in multi-user realtime applications. The number of cloud-based messaging services that include client-side pubsub has been growing significantly and for developers it can be difficult to make a decision which one to use. The services available are built using protocols such as WebSockets: some services offer the native WebSocket API directly while others have created abstractions on top of it, making it easier to use.
TLDR: Draw on the live canvas, or look at the source code in the GitHub repo.
Let's have a look at the technologies used in this application and expand on the cloud-based WebSockets providers.
Tech stack
I've created two versions of a collaborative drawing application. Both versions use these components:
- p5js: A creative coding library, used for the drawing canvas.
- Azure Functions: A serverless compute service from Microsoft Azure, used for the authentication endpoint, and a function to change the color palette.
- Azure Static Web Apps: An Azure service for hosting the static web app.
Serverless WebSockets
One version of the drawing application uses Ably, the other uses Azure Web PubSub. Both are cloud-based pubsub services (or Serverless WebSocket providers), used for realtime communication. Here's an overview of some features they offer.
Feature | Ably | Azure Web PubSub |
---|---|---|
pubsub messaging | Y | Y |
message queues | Y | N |
presence | Y | N |
connection state recovery | Y | Y1 |
automatic transport selection2 | Y | N |
guaranteed message ordering | Y | Y |
message history | Y | N |
webhooks | Y | Y |
1 Web PubSub reliable subprotocol is still in preview.
2 Ability to use the best transport available depending on client capablities.
The communication between clients and the cloud-based messaging service is shown in this diagram:
How I compare cloud pubsub services
I compare the two services by considering the following criteria:
- Creating the cloud resource
- Authentication
- Creating a client-side connection
- Presence
- Publishing messages
- Subscribing to messages
Naming
Before I start comparing the developer experience, let's have a look at naming first, since both cloud services sometimes use a different terminology for certain concepts.
Ably | Azure Web PubSub | Description |
---|---|---|
App | Service Instance | A cloud resource is usually created per application. This resource can contain multiple channels/hubs. |
Connection | Connection | An individual client connection (over WebSockets) that is connected to the cloud service. |
Channel | Hub | Logical group of client connections, usually for one purpose (e.g. chat). |
Presence | - | Awareness of the clients present in a channel. Clients can announce their presence by entering a channel, or remove their presence by leave a channel. Presence can be queried to retrieve a list of clients present in a channel. |
- | Group | A subset of client connections. Clients can join or leave a group. A Group can't be queried for their members. Messages can be sent to a group instead of an entire Hub. |
Integrations | Event Handlers | A method of adding extra functionality (e.g. calling a webhook, relaying a message to an upstream server) once messages are received at the cloud service. |
User | User | A user (person) uses a client (device) to establish a connection to the cloud service. One user can have multiple connections when using several clients. |
Client | Client | A client device that uses one connection to communicate with the cloud service. |
Message | Message | Information that is published by a user, sent to the cloud service and distributed to the connected clients. |
For more detailed info, see Web PubSub Service Internals and Ably Key Concepts.
Creating the cloud resource
Before we can do anything with cloud-based messaging, we need to create the appropriate resources.
Ably
For Ably, the first thing that has to be done is to create a new Ably app and an API key. If you've just signed up, a new default app and root API key is already created for you. I do suggest that you create a dedicated Ably app for each application you build. The same goes for the API key. The default root API key has all the capabilities that Ably offers and probably has too many rights. For this application, you only require Publish, Subscribe and Presence capabilities. The API key is required when using the Ably SDK for authentication.
Creating the Ably app and API key can be done via:
Web PubSub
For Web PubSub Service, you need to create a new Web PubSub service resource in Azure. You require an Azure subscription. When creating the cloud resource, you have to specify a resource group and a region to deploy the service. Once the service instance is created, you'll find the connection strings in the Keys blade in the portal. One of these connection strings is required when using the server-side Web PubSub SDK for authentication.
Creating the Azure resource can be done via:
- The Azure portal
- The Azure CLI
- Bicep templates
- Third-party infrastructure tools such as Terraform and Pulumi.
Resource creation differences
For the Azure resource, you need to define a region where the Web PubSub resource will be deployed. For Ably, you don't have to pick a region, it's multi-region by default (hosted on AWS). When a client connection is established, the Ably client will select a region that results in the lowest latency.
For Ably, you need to create an API key with the required capabilities, unless you're using the root key which has all the capabilities. For Web PubSub, there are connection strings (primary and secondary) and the Client URL Generator that is intended for test and validation purposes only.
Authentication
For a client to connect to the cloud service, it requires a client access token. Both Web PubSub and Ably provide SDKs to generate these tokens server-side, since it's not advised to have these tokens in client-side code. To generate the tokens, the SDKs require a connection string or API key to authenticate to the cloud service.
Ably
For Ably, an Azure Function is written in C# that uses the .NET ably.io NuGet package to create a client access token:
[FunctionName(nameof(CreateTokenRequest))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "CreateTokenRequest/{clientId}")] HttpRequestMessage req,
string clientId,
ILogger log)
{
var tokenParams = new TokenParams() { ClientId = clientId };
var tokenData = await _ablyClient.Auth.RequestTokenAsync(tokenParams);
return new OkObjectResult(tokenData);
}
The CreateTokenRequest
function depends on an Ably IRestClient
object via constructor injection. The dependency is configured in the StartUp
class:
public override void Configure(IFunctionsHostBuilder builder)
{
var ablyApiKey = Environment.GetEnvironmentVariable("ABLY_APIKEY");
var ablyClient = new AblyRest(ablyApiKey);
builder.Services.AddSingleton<IRestClient>(ablyClient);
}
See GitHub for the full implementation.
The Ably API key mentioned in the Creating the cloud resource section is put in the App Settings of the Azure Function App, so it can be retrieved as an environment variable, and used when an instance of the AblyRest
client is created.
Azure Web PubSub
For Azure Web PubSub, a similar CreateTokenRequest
Azure Function is created that uses the Azure.Messaging.WebPubSub NuGet package.
[FunctionName(nameof(CreateTokenRequest))]
public async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "CreateTokenRequest/{hubName}/{groupName}/{clientId}")] HttpRequestMessage req,
string hubName,
string groupName,
string clientId,
ILogger log)
{
var webPubSubService = new WebPubSubServiceClient(
Environment.GetEnvironmentVariable("WebPubSubConnectionString"),
hubName);
var clientUri = await webPubSubService.GetClientAccessUriAsync(
TimeSpan.FromMinutes(30),
clientId,
new[] {
$"webpubsub.joinLeaveGroup.{groupName}",
$"webpubsub.sendToGroup.{groupName}"
}
);
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(clientUri.ToString())
};
}
See GitHub for the full implementation.
The Web PubSub connection string key mentioned in the Creating the cloud resource section is put in the App Settings of the Azure Function App, retrieved as an environment variable, and used when an instance of the WebPubSubServiceClient
is created.
Authentication differences
The Ably specific function depends on the AblyRest
client, which only requires the Ably API key. The API key has the configured capabilities to allow publishing, subscribing and presence management for the channel. This means that if several different access rights are required for various roles, multiple API keys need to be created.
The Web PubSub specific function requires more parameters to create a client token:
- The connection string of the service.
- The Hub name
- The Group name*
- The client ID*
- The roles for leaving and joining the group*
* These are required since a specific subprotocol is used for the client connection. See the Creating a client-side connection section.
The Web PubSub connection string has all the rights to the service. When the client access token is generated, the group-specific access is defined.
Although the result is more or less the same (a client access token) the level of detail required for creating the connection is different.
Since the WebPubSubServiceClient
requires the Hub name next to the connection string, I decided not to use dependency injection for the service client. Otherwise, I had to put the Hub name in an environment variable as well. I believe that a Hub name should be truly dynamic, so I prefer it not be present in configuration or back-end code.
Creating a client-side connection
The user (with a client device) need to establish a connection with the cloud service. The client-side code will call the CreateTokenRequest
Azure Function shown earlier, that will return a client access token to establish the WebSocket-based connection.
Ably
Ably has a client SDK to manage connections and messaging, instead of using the native WebSockets API. An instance of the Ably.Realtime
client is created that uses the CreateTokenRequest
endpoint to obtain the client access token that contains the user ID.
const clientOptions = {
authUrl: `/api/CreateTokenRequest/${user.id}`,
echoMessages: false,
};
ably = new Ably.Realtime.Promise(clientOptions);
Once the ably
client is connected, the channel
can be retrieved:
channel = await ably.channels.get(channelName, {
params: { rewind: "2m" },
});
Note that the
rewind
parameter is used to retrieve the last two minutes of messages. This is possible because messages are persisted by Ably.
Azure Web PubSub
Azure Web PubSub uses the WebSocket API directly by specifying the URL with the client access token, and a subprotocol:
let tokenResponse = await fetch(
`/api/CreateTokenRequest/${hubName}/${groupName}/${user.id}`
);
let clientUrl = await tokenResponse.text();
webSocket = new WebSocket(clientUrl, "json.webpubsub.azure.v1");
See GitHub for the full implementation.
Client-side connection differences
For Ably, the JavaScript client library is used to create a WebSocket-based connection and interacting with the cloud service. The benefit of using this library over the native WebSocket API is that a lot of boilerplate WebSocket code and difficult to implement concepts, such as reliable connection management, and message ordering, are taken care of for you.
For Web PubSub, the json.webpubsub.azure.v1
WebSocket subprotocol is specified when creating the connection. This is required for doing pubsub without the need of using event handlers which push messages to an upstream server. The downside of using this subprotocol is that custom events can only be supported by using event handlers. In this application, I did not use any integrations/event handlers to reduce complexity.
Presence
When a user visits the web application and clicks the Connect button, they connect to the messaging service and their (randomly generated) client ID is added to a users
collection in the client-side code. The total number of connected users (client devices, actually) is shown next to the button:
How do you find out which (or how many) clients are connected to the channel? This concept is called presence.
Ably
With Ably, presence is a built-in feature that is exposed via the client SDKs.
The following four actions are done when a user has established a connection to Ably:
- Subscribe to presence enter messages.
- Subscribe to presence leave messages.
- Get the presence of the channel.
- Enter their own presence.
Subscribing to enter and leave presence messages published by other clients is done via the channel.presence
object:
channel.presence.subscribe("enter", (member) => {
addUser(member.clientId, member.data.color);
});
channel.presence.subscribe("leave", (member) => {
removeUser(member.clientId);
});
The entire channel presence can be retrieved as follows:
await channel.presence.get((err, members) => {
members.forEach((member) => {
if (member.data) {
addUser(member.clientId, member.data.color);
}
});
});
Retrieving the total presence of a channel is very useful since you don't have to store the connected users yourself. As soon as a client has joined a channel, they can retrieve the presence and know which users are present in the same channel.
A client can announce their own presence to the channel as follows:
channel.presence.enter({
color: user.strokeColor,
});
Note that you can submit a JSON payload when entering (or updating) presence information. This is ideal for storing client-specific data. In this case, the color of their pixel cursor.
See GitHub for the full implementation of user presence.
Azure Web PubSub
Although Web PubSub does have the Group concept and allows adding/remove users, it does not provide a way to retrieve user information from a Group. So, there is no presence awareness in Web PubSub.
Once a connection to the Web PubSub service has been established, these steps are performed:
- Send a joinGroup message, so the user is part of this group, and can send and receive group messages.
- Add user information to the
users
collection (to identify the user on the client-side). - Send a sendToGroup message to broadcast their user information to the rest of the group.
webSocket.send(
JSON.stringify({
type: "joinGroup",
group: groupName,
})
);
addUser(user.id, user.strokeColor);
webSocket.send(
JSON.stringify({
type: "sendToGroup",
group: groupName,
noEcho: true,
data: {
messageType: joinedMessage,
clientId: user.id,
color: user.strokeColor,
}
})
);
See GitHub for the full implementation.
However, one part is not yet accounted for. Say there are two clients, A and B, who are connecting in sequence (first A, then B) to the same Hub and Group. When B connects to the Hub, a sendToGroup message is published by B which is received by A to announce B has joined. But B has no idea that A is also connected, since B connected later to the Hub than A.
The only way for B to be aware of A is that A sends a sendToGroup message as well. The solution I chose was to always include the client ID and pixel cursor color when publishing the hoverPositionMessage (a user moves their mouse over the pixel canvas):
webSocket?.send(JSON.stringify({
type: "sendToGroup",
group: groupName,
noEcho: true,
data: {
messageType: hoverPositionMessage,
clientId: clientId,
color: this.strokeColor,
x: this.x,
y: this.y,
}
}));
See GitHub for the full implementation.
I don't really like this approach, since the payload gets a bit bloated with redundant information. Ideally, I'd only send x
and y
coordinates here.
Presence differences
The difference between Ably and Azure Web PubSub is significant when it comes to user presence. It would be great if Web PubSub would offer a way to list the users in a group. Currently, some workarounds are required to get the same functionality as is offered by Ably. On the other hand, the Group concept of Web PubSub has no equivalent in Ably. You could say that both Hubs and Groups map to Channels in Ably. So if you require multiple Groups, you can use multiple channels in Ably.
Publishing messages
The application publishes messages when:
- The user's mouse hovers over the canvas.
- The user clicks a pixel on the canvas.
- The user enters the 'r' key to reset the canvas.
- The user selects a color palette.
Scenarios 1 to 3 use client-side publishing. Scenario 4 uses server-side publishing (from the Azure Function). Scenario 4 could also have been implemented via client-side publishing, but I wanted to use the .NET SDK to see how it differs from JavaScript and what other options there are when using server-side code.
Client-side publishing
Let's look at the mouseClicked
function as an example of client-side publishing.
Ably
For Ably, the publish
method is used on the channel
object. The method requires an event name and a payload.
async function mouseClicked() {
if (mouseX >= 0 && mouseX <= resoX && mouseY >= 0 && mouseY <= resoY) {
clickCell(mouseX, mouseY);
await channel?.publish(clickPositionMessage, {
x: mouseX,
y: mouseY,
});
}
}
See GitHub for the full implementation.
Azure Web PubSub
For Web PubSub, the send
method is used on the WebSocket
object. The method takes a JSON string and requires a type
, groupName
, and a payload.
async function mouseClicked() {
if (mouseX >= 0 && mouseX <= resoX && mouseY >= 0 && mouseY <= resoY) {
clickCell(mouseX, mouseY);
webSocket?.send(JSON.stringify({
type: "sendToGroup",
group: groupName,
noEcho: true,
data: {
messageType: clickPositionMessage,
x: mouseX,
y: mouseY,
}
}));
}
}
See GitHub for the full implementation.
To distinguish between messages, I added the messageType
field to the payload. The noEcho
argument is optional, but since I don't want to echo the message back to the user, it is set to true
.
Client-side publishing differences
The Azure Web PubSub code is a bit more verbose since Groups are used. The usage of Groups is required though since the json.webpubsub.azure.v1
subprotocol is used because there was no need for me to send messages to an upstream server before sending them to the clients. Ideally, I want to use custom events using this subprotocol without the need of going through an upstream server. This is what Ably does by default by using the event name as the first argument.
The noEcho
can be configured for each send
method separately using Web PubSub. Although this is very flexible, I wonder how many use cases there are that require this flexibility. For Ably, the echoMessages
option is set when creating the Ably.Realtime
client and is applied across all publish
methods.
Server-side publishing
The ChangeColorPalette
Azure Function selects a color palette based on a palette ID in the route, and publishes a message to the cloud service.
Ably
For Ably, the publishing is done using the AblyRest
client that is injected via the constructor and assigned to the _ablyClient
parameter:
[FunctionName(nameof(ChangeColorPalette))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "ChangeColorPalette/{paletteId}")] HttpRequestMessage req,
string paletteId,
ILogger log)
{
string[] colors;
switch (paletteId)
{
// removed the switch cases to shorten this code snippet.
}
var channel = HttpUtility.ParseQueryString(req.RequestUri.Query)["channel"];
if (channel != null)
{
await _ablyClient.Channels.Get(channel).PublishAsync(
"color-palette",
new {
paletteId = paletteId,
colors = colors,
});
}
return new OkObjectResult(colors);
}
See GitHub for the full implementation.
A channel is obtained by name and the PublishAsync
method on that channel is called that takes an event name (color-palette) and a payload containing the paletteId
and the colors
array.
Azure Web PubSub
For Web PubSub, the WebPubSub
output binding is used:
[FunctionName(nameof(ChangeColorPalette))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "ChangeColorPalette/{groupId}/{paletteId}")] HttpRequestMessage req,
[WebPubSub(Hub = "{query.hubName}")] IAsyncCollector<WebPubSubAction> actions,
string groupId,
string paletteId,
ILogger log)
{
string[] colors;
switch (paletteId)
{
// removed the switch cases to shorten this code snippet.
}
var data = BinaryData.FromObjectAsJson(
new {
messageType = "color-palette",
paletteId = paletteId,
colors = colors,
});
var action = WebPubSubAction.CreateSendToGroupAction(groupId, data, WebPubSubDataType.Json);
await actions.AddAsync(action);
return new OkObjectResult(colors);
}
See GitHub for the full implementation.
In general, I really like input & output bindings in Azure Functions, since they offer a quick way to move data in and out of a function without writing much code in the function body. The WebPubSub
binding however still requires a few lines of code since the payload needs to be formatted, a WebPubSubAction
object has to be created, and this has to be added to the IAsyncCollector
.
An alternative would be to use the WebPubSubServiceClient
object with the SendToGroup
method in the function body instead of the output binding. It won't be much different regarding the lines of code required.
Server-side publishing differences
On the server-side, the amount of code required to publish a message is similar between Ably and Azure Web PubSub. I expected it to be less code for Web PubSub when using an output binding, but this wasn't the case. When looking at the steps required to publish a message, I find the Ably API a bit more concise and readable.
Subscribing to messages
The clients are subscribing to the following messages:
- hoverPositionMessage: When the mouse is moved over the canvas.
- clickPositionMessage: When the mouse is clicked on the canvas.
- changeColorPaletteMessage: When the color palette is changed.
- resetMessage: When the canvas is reset.
Ably
For Ably, subscribing to specific messages looks like this:
channel.subscribe(hoverPositionMessage, (message) => {
setUserPosition(message.clientId, message.data.x, message.data.y);
});
channel.subscribe(clickPositionMessage, (message) => {
clickCell(message.data.x, message.data.y);
});
channel.subscribe(changeColorPaletteMessage, (message) => {
handleChangeColorPalette(message.data.paletteId, message.data.colors);
});
channel.subscribe(resetMessage, () => {
resetGrid();
});
See GitHub for the full implementation.
In addition, the client can subscribe to presence events, when a user joins or leaves a channel:
channel.presence.subscribe("enter", (member) => {
addUser(member.clientId, member.data.color);
});
channel.presence.subscribe("leave", (member) => {
removeUser(member.clientId);
});
See GitHub for the full implementation.
Azure Web PubSub
For Azure Web PubSub, since it's using the native WebSocket API, the onmessage
event is used, which captures all kinds of events. It up to the developer to filter the exact type and since I added a separate messageType
field to the payload, that is used to identify the exact message.
webSocket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === "message") {
switch (message.data.messageType) {
case hoverPositionMessage:
setUserPosition(
message.data.clientId,
message.data.color,
message.data.x,
message.data.y
);
break;
case clickPositionMessage:
clickCell(message.data.x, message.data.y);
break;
case changeColorPaletteMessage:
handleChangeColorPalette(
message.data.paletteId,
message.data.colors
);
break;
case resetMessage:
resetGrid();
break;
case joinedMessage:
addUser(
message.data.clientId,
message.data.color);
break;
default:
break;
}
}
};
See GitHub for the full implementation.
Subscribing differences
The largest difference is the presence events, which are supported by Ably but not by Azure Web PubSub. Next to that, Ably supports event names that make it very convenient to subscribe to specific events. For Web PubSub, the lack of specific event subscriptions results in a large switch statement inside the onmessage
handler. Azure Web PubSub does support custom events, but these have to be processed with event handlers and sent to an upstream server, which adds additional complexity.
Summary
Both Azure Web PubSub and Ably offer a similar pubsub feature set. Ably provides some additional features not offered by Web PubSub, such as presence, message history, and transport selection. I missed presence the most when creating the application with Web PubSub.
Advantages of Ably
I found the Ably client-side API easier to develop with than the native WebSockets API. Using a higher level client SDK for realtime messaging removes a lot of complexity and makes the code more concise. I prefer writing less code and focusing on the (business) domain, so I'm happy with using client SDKs if they make my life easier.
Being able to publish named events and subscribe to them (client-side) without the need of handling them via an upstream server and custom event handlers is a big plus for me.
Potential disadvantages
A potential negative aspect of using a 3rd party library could be the download size of the client app. If you need to keep that to a minimum, then that would be an argument to choose native WebSockets.
I haven't touched on reliable message delivery, message ordering, and default multi-region support. The Ably SDK takes care of this for you completely, and there's no need to manage ackID
s, connectionIds
, and reconnectionToken
s yourself.
I do admit I'm biased, since I have used Ably more than I have used the native WebSockets API. But look at the code snippets in this post and decide for yourself.
I encourage you to clone/fork the GitHub repository and run the collaborative pixel drawing application yourself (either using Ably or Web PubSub). Try to add some extra pubsub features to it and let me know what your experience is.