Updated on
REST is the right default for client-server APIs: stateless request-response, cacheable, and understood by every tool in the ecosystem. WebSockets earn their complexity only when the server must push data to the client in real time: chat, live dashboards, multiplayer, price tickers.
In C# terms: ASP.NET Core controllers or minimal APIs for REST, and UseWebSockets() (or better, SignalR) for the persistent connection.
Understanding REST
REST (or Representational State Transfer) is an API architectural principle or style. It utilizes HTTP requests to perform standard database functions, also known as CRUD, on a particular resource – entity, or collection.
In a RESTful context, clients do not interact directly with the resource’s raw data. Instead, they can reach the resources through their representations. This approach separates the client from complex backend implementations.
Let’s implement a simple method for POST request:
[HttpPost]
public IActionResult AddTask([FromBody] string task)
{
if (string.IsNullOrEmpty(task))
{
return BadRequest("Enter valid data.");
}
Tasks.Add(task);
return Ok("Task added successfully.");
}
This is a REST endpoint representing the HttpPost verb. From our code, the resource representation is a simple JSON string, task. It encapsulates the current state of the resource and presents it to clients. In return, the client sends a request to add a new task to the resource by inputting a text for the task parameter.
The server responds with the status of the request – BadRequest if the parameter is null or empty or Ok if the task was added successfully.
Resource representations can take various forms, such as JSON, XML, HTML, or other data formats. Typically, any data sent outside the specified format or state is considered invalid.
Note that while REST encourages well-defined representations, the handling of invalid data is typically determined by the specific API’s design and validation rules.
Characteristics of REST
In the context of RESTful APIs, communication is characterized as both stateless and unidirectional. This approach adheres to a request-response model in which client applications take the initiative to communicate with the server. Each client request is self-contained, encompassing all the necessary information for the server to comprehend and execute the requested action. The key aspect of statelessness within REST is that the server does not retain any memory or awareness of prior client requests or sessions. As a result, each interaction between the client and server remains entirely independent.
To enhance server response times and overall application scalability, REST supports caching. Caching involves storing and reusing previously fetched or computed data, which can significantly reduce the need for repetitive, resource-intensive processing. By caching responses, servers can deliver frequently requested data more swiftly, improve overall application performance, and reduce the load on server resources. Caching is particularly beneficial when data doesn’t change frequently and we can use it for multiple clients, promoting efficiency and responsiveness.
REST is an excellent choice for exposing APIs to enable other applications to interact with your system. Its adherence to HTTP standards makes it particularly beneficial when you need to perform operations like creating, reading, updating, and deleting data in a database.
APIs that conform to these principles are known as REST or RESTful APIs.
Disadvantage of REST
All things considered, however, REST is not the optimal choice for real-time applications where constant updates are critical such as live web chats or trading applications.
Owing to its unidirectional state of communication, current updates in a RESTful API would require repeated requests sent to the server at different intervals by the client. Clients can only make new requests after the server has responded to the previous ones. Over time, this will take a toll on the server.
For such applications, we have alternative protocols and architectures, such as WebSockets.
Understanding WebSockets
WebSockets address exactly this. WebSocket technology provides a seamless way to construct real-time applications while avoiding the overhead typically associated with traditional APIs. This approach enables bidirectional and stateful communication between the client and server by maintaining a persistent, long-lived connection. Hence, the two apps can send data to each other without repeated polling required in REST. There are no templates to organize the type of data sent, therefore clients can send data to the server without the need for an explicit request.
Let’s create a new ASP.NET Core application so we can define a WebSocket endpoint.
But first, we have to set up WebSocket middleware in the Program class:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddOpenApi();
builder.WebHost.UseUrls("http://localhost:5289"); // Client/Program.cs hardcodes this URL
var app = builder.Build();
app.MapOpenApi();
app.UseWebSockets();
app.MapControllers();
app.Run();
Notice that AddOpenApi() and app.MapOpenApi() take the place of AddSwaggerGen() and the Swashbuckle middleware for generating the OpenAPI document only — an interactive UI like Swagger UI or Scalar still needs a separate package — while UseWebSockets() stays exactly where it was.
After calling the app.UseWebSockets() method, our application is configured to recognize and manage WebSocket requests, allowing us to handle WebSocket communications within our application.
In the controller, let’s implement Get(), a WebSocket endpoint:
[Route("/ws")]
[HttpGet]
public async Task Get()
{
using var ws = await HttpContext.WebSockets.AcceptWebSocketAsync();
while (ws.State == WebSocketState.Open)
{
var message = $"The time is: {DateTime.Now:HH:mm:ss}";
var bytes = Encoding.UTF8.GetBytes(message);
await ws.SendAsync(bytes, WebSocketMessageType.Text, true, CancellationToken.None);
await Task.Delay(1000);
}
}
Notice /ws, the route path and HttpGet attribute.
We accept the connection with AcceptWebSocketAsync() and keep sending while the socket stays open. The endpoint expects a WebSocket upgrade request; anything else belongs to normal controllers.
In the while loop, we repeatedly send a message with the current time in “HH:mm:ss” format to the connected WebSocket client, waiting one second between sends — using await Task.Delay(1000) rather than blocking, the same fix our article on Thread.Sleep vs Task.Delay covers in detail. The loop keeps running as long as the socket stays open, and ends on its own once it doesn’t.
Now, let’s consume this endpoint in the Program class of a Console application:
var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri("ws://localhost:5289/ws"),
CancellationToken.None);
Console.WriteLine("Connected!");
var receiveTask = Task.Run(async () =>
{
var buffer = new byte[1024];
while (true)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer),
CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
break;
}
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine("Received: " + message);
}
});
await receiveTask;
First, we have a new instance of a ClientWebSocket. After successfully establishing the WebSocket connection, it prints “Connected!” to the console. We run an asynchronous operation to receive messages from the WebSocket server. As long as the connection persists, we repeatedly print the time to the console.
Characteristics of WebSockets
WebSocket is a communication protocol. Endpoints that implement WebSockets do not require polling. They operate on an event-driven model, delivering data promptly as it becomes available.
Like a phone call, the connection enabled by WebSockets persists until either party ends it. Data goes back and forth seamlessly. Additionally, the server does not require the client to resend previously shared data because the server is enabled to retain state.
Due to this persistent connection over the request/response cycle of regular HTTP APIs, WebSockets require lower latency and less bandwidth. This reduces the load on the server.
WebSockets enable a duplex or bidirectional communication. This means that clients and servers send data to each other concurrently.
Disadvantages of WebSockets
Every current browser supports WebSockets natively, so browser compatibility is not the constraint it once was. The real constraints are proxies and corporate networks that do not always handle persistent connections well, and the statefulness the server must maintain for every open connection.
Additionally, the implementation and integration of WebSockets are quite complex. We are responsible for managing the connection’s lifecycle, and errors, and ensuring we handle messages properly.
Although the persistent connection is a distinctive advantage WebSocket holds over REST, insufficient management and the absence of a set time-out pose a significant risk of prolonged idle open connections. Consequently, their stateful nature and the maintenance of persistent connections contribute to resource and memory consumption.
REST vs WebSockets: Which Should We Choose?
We choose REST unless the application needs server push. A REST API is stateless, so it scales horizontally without session affinity, caches at every layer HTTP offers, and works with every client, proxy, and monitoring tool ever built.
Those properties disappear with WebSockets: each open socket is state the server must hold, load balancers need sticky routing or a backplane, and we own the connection lifecycle: reconnection, keep-alives, and error handling. That cost is justified when data must reach the client the moment it changes, because the REST alternative is polling, which wastes requests and still delivers stale data between calls.
The practical rule: request-driven interactions (fetch, create, update, delete) stay REST; event-driven flows (notifications, live prices, chat, presence) go over a socket. Most real systems use both: a REST API for resources, plus one WebSocket or SignalR endpoint for the live parts.
When Should We Use WebSockets in C#?
In C#, WebSockets make sense when clients must see changes within a second or two of them happening: live dashboards, chat, collaborative editing, game state, or telemetry streams. ASP.NET Core supports them natively: we call app.UseWebSockets(), accept the connection with HttpContext.WebSockets.AcceptWebSocketAsync(), and exchange frames with SendAsync()/ReceiveAsync(), as our example shows.
For production applications, though, we rarely stay at that raw level. SignalR builds on WebSockets and adds the parts we would otherwise write ourselves: automatic reconnection, per-user and group messaging, a typed hub protocol, scale-out backplanes, and transport fallback for clients that cannot open a socket.
Raw ClientWebSocket is the right choice when we consume a third-party socket API or need full control of the wire format; SignalR is the right choice for browser-facing real-time features in our own applications. For one-way server push, Server-Sent Events is a simpler alternative worth considering.
Learn more about calling a SignalR hub from a controller and sending client-specific messages with SignalR. If we’re deciding between a WebSocket endpoint and a set of minimal APIs for the rest of the surface, minimal APIs remain the right default for everything that isn’t push.
Comparing REST and WebSockets in C#
Let’s see a side-by-side comparison of REST and WebSockets:
| REST | WebSocket |
|---|---|
| REST is stateless. | WebSocket is stateful. |
| REST APIs are compatible with most browsers and can easily be integrated. | Might require extra client/server support upon integration.It is also not compatible with some old browsers. |
| Best suited for CRUD operations on resources. | Best for real-time updates. |
| Can be scaled using additional server instances to handle increasing load. | Requires less server-side scaling for large numbers of concurrent clients since they maintain persistent connections. |
| Incurs more overhead due to the stateless nature of communication. | Overhead is lesser because initial communication set up occurs just once. |
That row is the specification’s own argument for the protocol: RFC 6455 §1.1 describes the polling alternative as one where “the wire protocol has a high overhead, with each client-to-server message having an HTTP header.”
Conclusion
It is of the utmost importance to select the right means of communication for our application. In general, WebSockets often take the spotlight, particularly in real-time applications. However, our choice between REST and WebSockets hinges on our application’s specific use cases and performance requirements. These considerations encompass data transfer patterns, statefulness, security needs, compatibility, resource handling, use cases, and scalability. As a result, our mode of communication must closely align with our vision for our application.
Tested with .NET 10.0.10.
