Field note 02Real-time systems

What enterprise SignalR work taught me about live products.

WebSockets solve transport. The product still has to decide what is current, what was missed, who may subscribe, and what the interface should say when the connection lies.

Published
December 15, 2025
Reading time
7 minutes

A live interface can fail while looking healthy. The socket is connected, but the user joined the wrong group. An event arrived out of order. The browser slept for ten minutes and missed a transition. The dashboard still shows a green dot.

Working on enterprise event flows with SignalR changed how I think about real-time products. The difficult work sits above the protocol: state ownership, access, recovery, and honest feedback. These lessons apply whether the transport is SignalR, raw WebSockets, server-sent events, or a managed realtime service.

01 / State

An event is not the current state.

“Vehicle updated”, “analysis completed”, and “alert assigned” describe transitions. A newly opened screen still needs a trustworthy snapshot. If the interface builds its entire state by replaying whichever messages happened to arrive, refreshes and reconnects become guesswork.

A safer flow is snapshot first, updates second: load the authorised current view through a normal query, record its version, then apply newer events. The event moves the interface forward; it does not become the only record of truth.

{
  "stream": "vehicle:42",
  "version": 1842,
  "occurredAt": "2026-01-18T09:42:13Z",
  "type": "position.updated",
  "payload": { "latitude": 36.81, "longitude": 10.18 }
}

02 / Scope

Subscriptions need the same access rules as reads.

Joining a group is a read operation that stays open. It should be authorised with the same organisation, project, fleet, or resource boundary used by the rest of the product. Guessable group names and client-supplied organisation IDs are not an access model.

Narrow subscriptions also reduce noise. A screen showing one analysis run should not receive every event in the project. A fleet operator should subscribe to the vehicles and alerts currently in scope, not a global stream that the browser filters afterward.

03 / Recovery

Reconnect is a reconciliation flow.

Automatic reconnect restores the transport. It does not prove that the interface is current. The client may have missed one event or one hundred. Its previous subscriptions may no longer be valid. The user's permissions may have changed while the connection was down.

On reconnect, rebuild the authorised subscriptions and compare a version, timestamp, or cursor. If the gap is small and the server keeps an event window, backfill it. Otherwise, request a fresh snapshot. The recovery path should be deterministic enough to test without unplugging Wi-Fi by hand.

Transport recovered

The connection is open again.

Product recovered

Subscriptions, permissions, and visible state have been reconciled.

04 / Interface

Freshness belongs in the UI.

Real-time status is product information. “Live”, “reconnecting”, “updated 40 seconds ago”, and “showing last known value” lead to different decisions. Hiding the connection state makes the interface look cleaner right up until somebody acts on stale information.

Not every update deserves animation or a toast. Often the right behaviour is quiet: update a row in place, preserve the user's scroll and selection, and expose freshness beside the value. Reserve interruption for changes that require a decision.

A live product should feel calm because the state model is clear—not because failure is hidden.

05 / Operations

Observe the path from event to visible change.

Connection count alone says very little. Useful signals include rejected subscriptions, reconnect frequency, delivery lag, queue depth, dropped or superseded messages, and the age of the state currently shown to the user. Correlation IDs should survive from the domain event through the hub and into client logs.

Load tests should model actual subscription patterns, not thousands of idle sockets. A realistic test reconnects clients, changes groups, sends bursts, slows consumers, and verifies that the final visible state is correct.

The result is not “a WebSocket feature.” It is a product surface with an explicit contract for freshness, access, recovery, and failure. SignalR is useful infrastructure for that contract, but it cannot define the contract for you.

Keep these

The short version.

  1. 01

    Treat an event as evidence of change, not as the complete current state.

  2. 02

    Authorise subscriptions against the same domain boundary as ordinary reads.

  3. 03

    Design reconnect as a reconciliation flow with versions or cursors.

  4. 04

    Show users when information is live, delayed, reconnecting, or stale.

Continue reading

From ambiguous brief to production software