The requirement almost always arrives in the same shape: "we want it to update live, without refreshing the page." It sounds like a detail — all we want is for a number on screen not to lag behind reality. In practice it is one of the decisions that most changes the architecture of the application, the cost of running it, and everything the team has to watch after launch.
The difference is fundamental. An ordinary web application answers questions: a request comes in, a response goes out, the connection closes and the server forgets the client. A real-time web application has to maintain a relationship with every open browser — knowing who is connected, what they care about, what it has already sent them and what it failed to send. This article is about approaching that requirement soberly: when it makes sense, what the options are, what each one costs, and what traps wait for you the moment a connection drops or two people edit the same record.
Do you really need real-time, or just fresher data?
The first question is not technical but product-level: what actually goes wrong if a user sees a value that is fifteen seconds old? For a large share of business applications the answer is "nothing". A manager looking at an overview does not need the numbers to change before their eyes — they need them not to be yesterday's. A dispatcher assigning vehicles, or two people in the same form, are a different case; there, delay directly causes collisions and duplicated work.
It helps to sort your screens into three groups:
- Genuine real-time (sub-second). The decision is made immediately and several people work on the same object: chat, concurrent editing, dispatching, monitoring with alarms.
- Fresh data (seconds to tens of seconds). Order lists, task statuses, operational overviews. The user tolerates a short delay, they just do not want to click refresh.
- Batch views. Reports, closings, period summaries. Here "liveness" is a drawback — numbers that change while you read them are hard to interpret and even harder to compare.
The second group is by far the most common in practice, and periodic fetching is almost always enough for it. The client asks the server every few seconds whether anything is new; with conditional requests (ETag, If-Modified-Since) the answer is usually empty and cheap. When the tab is in the background, the interval stretches or stops. No new infrastructure appears, it passes through every corporate proxy, failures are debugged with ordinary tools, and retrying is built into the principle itself. If the goal is mainly for management to see current figures without a manual export, we cover that in more depth in the piece on real-time reporting for company management.
Polling, SSE and WebSockets: what each option costs
If you have passed the filter above and genuinely need real-time, choosing the transport layer is mostly an operational decision, not a speed one. All of the options deliver data fast enough; they differ in what they ask of you every day afterwards.
| Approach | Data flow | Typical latency | Operational burden | When it is a good fit |
|---|---|---|---|---|
| Periodic polling | client asks | the interval (e.g. 5–30 s) | lowest | overviews, lists, task statuses |
| Long polling | client asks, server holds | near-instant | medium | when you need speed but not two-way traffic |
| Server-sent events (SSE) | server → client | near-instant | medium | notifications, live charts, streamed responses |
| WebSockets | two-way | lowest | highest | chat, concurrent editing, games, dispatching |
| Hosted real-time service | provider-dependent | near-instant | low in development, higher in cost | a small team that does not want to run its own layer |
Server-sent events are the most consistently underrated option in the projects we see. It is a plain HTTP connection that the server keeps open and writes lines of text into. Automatic reconnection and message numbering are part of the standard, it works over ordinary HTTP infrastructure, and no separate protocol appears on the server side. If you only need the "server to user" direction — and for notifications and live charts that is exactly the case — WebSockets are often needlessly heavy.
Choose WebSockets when the client genuinely sends a lot of messages back and the ordering and latency of both directions matter. The price is your own protocol on top of the connection, your own authentication and authorisation for every message, and having to solve what HTTP solves for you. Ultimately the difference between the technologies is smaller than the difference between "we have it switched on" and "we know how to run it", a topic we also address when designing custom web applications.
Every open connection is a small piece of state you have to hold somewhere, pay for, and eventually close properly.
State that falls apart: why real-time UI needs reconciliation
This is where most first implementations fail. A developer tests the feature on office wi-fi, sees the messages arriving and considers the job done. Reality is different: the mobile network switches over, the laptop goes to sleep, the corporate proxy kills an idle connection after a minute, a new deployment disconnects everyone at once. And the user gets no warning — the screen simply stops being alive while pretending to be current.
Hence a simple rule: the message stream is not the source of truth, it is only an optimisation. The server remains the source of truth, and the interface must have a way to align with it at any moment. In practice that means three things:
- Detection. A regular heartbeat in both directions and a visible connection indicator. If the user sees "reconnecting", they will not make a decision on top of dead data.
- Recovery. After reconnecting, either fetch the missing events from the last known sequence number, or — simpler and entirely sufficient for most applications — reload the whole screen state and only then start applying the stream again.
- Idempotency. Assume a message will arrive twice or out of order. Handling an event must be safe to repeat, otherwise duplicates appear in your lists that nobody can explain.
Optimistic updates and conflicts during concurrent editing
Optimistic UI — showing the change immediately, before the server confirms it — is what makes an application feel responsive. It is also the most common source of silent inconsistency. Every optimistic change needs three states: pending, confirmed, rejected. A rejection has to be able to roll the change back and say so clearly, not quietly overwrite it.
For concurrent editing you choose between three modes. The simplest is a lock on the record — while someone is working on it, everyone else sees read-only; inconvenient, but predictable. The second option is record-level versioning: the client sends the version it saw and the server refuses a write over a newer version. The third and most expensive option is field-level merging or data structures designed for automatic merging, which you realistically need only for collaborative text.
Scaling the real-time layer
As long as a single application server is running, everything is simple: an event occurs and the server pushes it to its connected clients. The moment you add a second server, this stops working — a user connected to server A never hears about an event created on server B. The solution is a distribution layer through which the servers relay events to each other: a message broker or a publish/subscribe channel.
Three more things come with it, and they need solving before production rather than after:
- Sticky sessions and timeouts. The load balancer has to know the connection must stay on the same node, and its idle timeout must be longer than the heartbeat interval.
- Connection limits. Each connection consumes memory and a system descriptor. Node capacity is therefore planned by the number of people signed in at once, not by requests per second.
- The post-deployment surge. A restart disconnects everyone at once and everyone tries to reconnect at once. Without exponential backoff with random jitter, the application takes down its own startup.
Do not forget authorisation either. With classic HTTP, permissions are checked on every request; with a long-lived connection they are checked once at connect time, after which a person can lose access and still keep receiving data. The check has to happen when each message is sent. We cover the broader context of growing load in the article on scaling a web application as the company grows.
Notification design: channel, batching and attention
The technical side of notifications is easier than the product side. The decision that determines success is this: which event deserves to interrupt a person, and which should quietly wait inside the application.
| Channel | Suited to | Main risk |
|---|---|---|
| In-app list | routine events, work context | the user may never open it |
| Browser push | time-sensitive events | permission is asked once, a refusal is permanent |
| digests, things for later | easily ends up in a forgotten folder | |
| SMS and calls | critical failures, on-call | expensive and intrusive; for a narrow group only |
A few principles that hold regardless of industry: never notify a person about their own action; merge related events into one message after a few minutes instead of sending five separate ones; offer settings per event type, not a single switch for everything; and sync "read" state across devices, or the user deals with the same thing twice. If you are considering push notifications without a native app, that is in practice the topic of progressive web applications — and be aware that support still differs across platforms.
The operational tax: what real-time adds to monitoring and testing
Real-time features bring a class of failure that classic monitoring never catches. The server responds, the error rate is zero, the page loads fast — and yet half the users are looking at stale data because their connection dropped an hour ago and never came back. So you need to measure different things: the number of active connections, the reconnection rate, the delay between an event being created and delivered, the number of undelivered messages, and the queue depth in the broker.
Testing is a similar chapter. Automated tests must be able to open two sessions at once and verify that an action in one shows up in the other; they must be able to sever the connection artificially and check that state was reconciled. A load test has to simulate a thousand simultaneously open connections, not a thousand requests per second — those are entirely different scenarios. How such scenarios fit into the overall quality process is described in the piece on the QA process before a software launch.
Count on the real-time layer being a source of cost that does not disappear once the project is handed over. Open connections are paid for in time, not requests, so the monthly bill grows with the number of signed-in people even when nobody is doing anything.
Summary
A real-time web application is not a feature you "add at the end" — it is a decision about architecture, operations and budget for years to come. The approach that works is boring: first name the specific decision that delay would ruin; if there is none, use polling and be done. If there is one, pick the simplest layer that suffices — for server-to-user traffic that is usually server-sent events. Then, before the first line of UI, design how state gets reconciled after an outage, and only at the end deal with optimistic updates and conflicts.
Design notifications as a product, not as a technical by-product: fewer messages in the right channel are always worth more than everything instantly everywhere. And expect monitoring and testing to cost more than in an ordinary application — that is the tax you pay for a screen that stays alive.
If you are weighing whether real-time makes sense in your application and to what extent, we are happy to go through it screen by screen — get in touch and we will start with what genuinely has to happen immediately.