HTML over WebSockets: what Blazor Server actually gives you

A popular Hacker News post celebrates HTML over WebSockets as the way to build SPAs with barely any JavaScript. For .NET teams that already exists: it's Blazor Server. Here's the field view, with the costs nobody puts on the slide.

Jean-Pierre Broeders

Freelance .NET Developer

August 13, 202610 min. read
HTML over WebSockets: what Blazor Server actually gives you

There's a post riding high on Hacker News this week that repackages an old idea: stop shipping JSON to the browser so a framework can assemble HTML there, render the HTML on the server instead, and push it into place over an open connection. The author lines up Phoenix LiveView, Hotwire and Laravel Livewire and calls the whole approach "real-time SPAs with barely any JavaScript." And somewhere in that table, between Elixir and PHP, sits C# with Blazor Interactive Server.

That one row deserves more than a footnote. Because if you write .NET, this isn't a preview of the future. Blazor Server has done it since 2019, and I've watched enough of it run in production to know where the approach sings and where it hurts.

The idea is right, the marketing isn't

The promise is seductive. One language, one render engine, no API contract to maintain on two sides, no separate frontend build that swaps frameworks every six weeks. Your business logic and your UI logic live in the same project, in the same language, and the user gets an interface that feels like a SPA.

Where the post moves too fast is the phrase "barely any JavaScript." It's literally true and it's misleading. You don't write JavaScript, but the complexity doesn't vanish. It relocates. From the browser to the server, and from your code to the place you can see it worst: the network and the memory of your app servers. That's a real trade with real consequences, and they're worth looking at soberly before you put a team on Blazor Server.

What Blazor Server literally does

Here .NET diverges from LiveView, and it's a difference that matters. LiveView and Hotwire send HTML fragments down the wire. Blazor Server sends no HTML. It keeps a render tree on the server per connected user, and on every UI change it computes a diff of that tree. Only the diff crosses the WebSocket, as a binary RenderBatch. The JavaScript runtime in the browser is small and dumb: it receives the diff and patches the DOM.

That connection is called a circuit. One circuit per browser tab, carried by a SignalR hub, and for as long as the tab stays open the full component state of that user lives in your server's memory.

A counter shows the minimalism.

@page "/counter"
@rendermode InteractiveServer

<h1>Count: @count</h1>
<button @onclick="Increment">Add one</button>

@code {
    private int count;

    private void Increment() => count++;
}

No fetch, no endpoint, no JSON, no state-management library. You click, and count goes up. What the slide doesn't show: that click travels to the server, the server runs Increment, computes the diff, sends it back, and the browser repaints. On an office network you feel none of it. For a user on 4G on a train, every click is a round trip to Amsterdam and back.

The latency is in the interaction, not the load time

This is the trap people fall into. They test Blazor Server on localhost, where the round trip is zero milliseconds, and conclude it feels fast. On localhost everything feels fast.

Put the same app behind a hundred milliseconds of network and its character changes. Every interaction the server has to see gets delayed by that hundred milliseconds. A single click is fine. A text field that validates on every keystroke becomes torture, because you send an event per letter.

<input @bind="filter" @bind:event="oninput" />

With oninput a message goes to the server on each keystroke. On a slow line you watch your own typing lag behind. The reflex is to fix this client-side, at which point you're writing exactly the JavaScript you set out to avoid. The honest fix is to debounce, or react on onchange instead, and accept that some interactions simply belong in the browser. Blazor WebAssembly and a bit of real JS through IJSRuntime exist for that reason.

Memory is the second bill

Every circuit costs memory. Component instances, their fields, the previous render tree to diff against, buffers for batches not yet acknowledged. For ten concurrent users that's nothing. For ten thousand it's a capacity question you answer up front, not when the server tips over.

By default Blazor holds a disconnected circuit for a while so a user who briefly loses wifi comes back to the same place. Nice for the user, but it means closed tabs keep memory reserved for a bit. Those knobs are worth setting on purpose.

builder.Services.AddServerSideBlazor()
    .AddCircuitOptions(o =>
    {
        o.DetailedErrors = false;
        o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(2);
        o.DisconnectedCircuitMaxRetained = 100;
    })
    .AddHubOptions(o => o.MaximumReceiveMessageSize = 64 * 1024);

And because the state lives on one specific server, every request from a user has to reach that same server. Run more than one instance and you need sticky sessions, or you put the Azure SignalR Service in the middle as a backplane. Do neither, and a user loses their circuit the moment the load balancer sends them to another pod, which happens at the worst possible time.

When I reach for it

Blazor Server is at its best for internal tooling. An admin panel, an order dashboard, a back-office app for a handful up to a few hundred users on a fast network. There the latency is negligible, the memory is a non-issue, and the payoff is huge: you build a rich, interactive UI without touching a single API endpoint or frontend framework. I've shipped an internal platform in weeks that way, where a React-plus-API version would have taken months.

For a public site with tens of thousands of visitors on mixed networks, I wouldn't. The per-interaction latency and the per-connection memory work against you there. That's where an approach closer to the HN post scales better.

The stateless cousin: htmx with plain Razor

The post names htmx as the HTTP variant of the same family, and it pairs beautifully with ASP.NET Core. Your server renders an HTML fragment, htmx fetches it and swaps it into the DOM. No circuit, no server state, no sticky sessions. Just a request, a partial, a response.

<div hx-get="/orders/live" hx-trigger="every 3s" hx-swap="innerHTML">
  <p>Loading...</p>
</div>
app.MapGet("/orders/live", async (IOrderQuery query) =>
{
    var orders = await query.RecentAsync();
    // Razor partial rendered to a string, returned as HTML.
    return Results.Extensions.RazorPartial("_OrderRows", orders);
});

This scales like any other stateless HTTP endpoint, because that's what it is. You pay with a little JavaScript (the htmx library, eleven kilobytes) and with the fact that true real-time push still needs something extra. For most "refresh this list every few seconds" screens, polling with htmx is plenty, and infinitely easier to operate than a fleet of circuits.

When you genuinely need push: SignalR directly

For a dashboard that has to update live without polling, or a chat, or an order screen that pings the moment something lands, reach for SignalR directly. It's the same transport Blazor Server runs on, minus the per-user component state. You decide what goes over the wire.

using Microsoft.AspNetCore.SignalR;

public sealed class MetricsHub : Hub;

public sealed class MetricsBroadcaster(IHubContext<MetricsHub> hub)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var snapshot = await ReadSnapshotAsync(ct);
            await hub.Clients.All.SendAsync("metrics", snapshot, ct);
            await Task.Delay(TimeSpan.FromSeconds(2), ct);
        }
    }

    private static Task<object> ReadSnapshotAsync(CancellationToken ct) =>
        Task.FromResult<object>(new { cpu = 41, queue = 7 });
}

On the client that's a handful of JavaScript lines catching a metrics event and updating a counter. You deliberately send JSON here, not HTML, because a number isn't worth a chunk of markup. The lesson from the HN post isn't that HTML over the socket is always the win. It's that an open connection gives you options, and you get to pick per screen what you send across it.

The real trade-off

The trend Hacker News is celebrating is real, and .NET has been living in it for years. But "barely any JavaScript" is a price tag that leaves off half the cost. You swap client complexity for server state, per-connection memory and per-interaction latency. That's a good swap for internal apps on fast networks and a bad one for public apps on slow ones. The skill isn't in picking a camp. It's in knowing, per screen, which of the three, Blazor Server, htmx or bare SignalR, fits that screen.

Frequently asked questions about HTML over WebSockets in .NET

Does Blazor Server really send HTML over the WebSocket? No, and that's the difference from LiveView and Hotwire. Blazor Server keeps a render tree on the server per user and on every change sends only a binary diff of that tree, a RenderBatch, over the SignalR connection. The browser receives the diff and patches the DOM. That's usually leaner than shipping whole HTML fragments, but it does mean the server holds the full UI state of every active user in memory.

What's the difference between Blazor Server and htmx for a .NET team? Blazor Server is stateful: every user has a circuit with server state, which gives you interactive UI without APIs but costs memory and sticky sessions. htmx is stateless: your server renders an HTML fragment per request and htmx swaps it into the DOM, exactly like any other HTTP endpoint. For rich internal apps on a fast network Blazor Server wins on convenience, for public screens that must scale simply htmx with Razor wins.

Why does my Blazor Server app feel slow when typing? Because every interaction the server has to see is a network round trip, and a text field with oninput sends a message per keystroke. On localhost you don't notice, on a real connection with a hundred milliseconds of latency your typing lags behind. Debounce the input, react on onchange instead of oninput where you can, or handle purely visual interactions in the browser itself.

Do I need the Azure SignalR Service for Blazor Server? Once you run more than one server instance, yes, or you set sticky sessions on your load balancer. A user's circuit state lives on exactly one instance, so every following message has to reach that same instance. The Azure SignalR Service takes over those connections as a backplane so you can scale horizontally without users losing their circuit during a rebalance.

When should I use SignalR directly instead of Blazor Server? When you want true server push without the per-user component state: a live dashboard, a chat, a screen that must update the moment an event arrives. Then you decide what crosses the wire, often a compact JSON payload rather than UI diffs, and you don't pay for a full circuit per connected tab. It's the same transport that sits under Blazor Server, just without the state bill.

Further reading


Conclusion: HTML over WebSockets isn't a new trick for .NET, it's a familiar one with a price tag. Reach for Blazor Server on internal apps on fast networks, htmx with Razor on public screens that must scale simply, and bare SignalR when you genuinely need push. Deciding per screen beats picking a camp.

Sources: HTML over WebSockets · Hacker News discussion.

Want to stay updated?

Subscribe to my newsletter or get in touch for freelance projects.

Get in Touch