155: Real-Time Delivery: SSE, WebSockets, Polling, Queues, and Presence
Learning outcomes
By the end of this lesson, you can:
- explain and apply polling in a realistic implementation;
- explain and apply server-sent events in a realistic implementation;
- explain and apply websockets in a realistic implementation;
- explain and apply event identity and ordering in a realistic implementation;
- explain and apply presence in a realistic implementation.
These outcomes are about making and defending an implementation choice, not about memorizing a list of APIs. You should be able to connect a product requirement to a delivery mechanism, describe the contract that mechanism provides, and identify what happens when the network, client, or server behaves imperfectly.
Prerequisites and retrieval
This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before reading, retrieve one concrete example from a previous project where this concern appeared. Perhaps a dashboard refreshed periodically, a notification stream stayed open, a chat connection dropped, or several browser tabs displayed the same changing state. The point is to make the terminology attach to a system you have actually seen.
The goal is not to memorize terminology. It is to make a defensible decision inside a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns. Keep those boundaries in view: a browser connection is not the same thing as durable storage, and delivering an event is not the same thing as proving that the event was processed.
Terminology
- Polling: Polling is easy to deploy and cache, but it trades freshness for repeated requests. The client asks for the current state on an interval or in response to a trigger instead of keeping a delivery connection open.
- Server-Sent Events: SSE provides long-lived, server-to-browser event delivery over HTTP with automatic reconnection semantics in
EventSource. It is a good fit when the server needs to push updates in one direction. - WebSockets: WebSockets provide bidirectional messages over a persistent connection. Both sides can send messages, which is useful when the interaction itself is continuous rather than request-oriented.
- Event identity and ordering: Assign event identifiers or versions so clients can deduplicate, resume, and reject stale updates. Arrival order alone is not a reliable substitute for a meaningful ordering rule.
- Presence: Online/offline presence is approximate because disconnect detection is delayed. A client that disappears may remain apparently online until a heartbeat expires.
- Horizontal fan-out: Multiple API instances need shared pub/sub, a broker, or another routing layer so an event created on one instance reaches clients connected to others. An in-memory listener on one process cannot solve that cross-instance routing problem.
The useful distinction is between state retrieval and event delivery. Polling repeatedly retrieves state. SSE and WebSockets deliver messages over a connection, but the application still needs a way to recover state after a missed message or reconnect. Queues and brokers can help move work or events between producers and consumers; they do not, by themselves, define what a browser should display or how a client proves that it has caught up.
Mental model
Treat Real-Time Delivery: SSE, WebSockets, Polling, Queues, and Presence as a design problem with observable inputs, outputs, invariants, and failure modes. “Real-time” is a product latency requirement, not a default architecture. Choose the simplest mechanism that meets the required directionality, fan-out, ordering, and reconnection behavior.
For example, if a progress page can be several seconds behind and the current state is cheap to read, polling may be the most reliable design. If the server needs to stream one-way progress updates promptly, SSE may fit better. If both sides exchange frequent messages, WebSockets may be appropriate, but the connection lifecycle and recovery work become part of the design. A strong implementation makes these assumptions visible, narrows uncertainty at boundaries, and leaves enough evidence—tests, types, constraints, metrics, or diagrams—to show why the design is safe.
A useful interview and production sequence is:
requirement -> constraints -> model -> implementation -> failure analysis -> verification
Do not jump from a requirement directly to a library call. First state what must remain true. Then choose the mechanism that enforces it. A useful invariant might be “a client never applies an older version over a newer one,” or “a reconnecting client can obtain the state it missed.” The transport is only one part of making that invariant hold.
Deep dive
1. Polling
Polling is easy to deploy and cache, but it trades freshness for repeated requests. The client asks the API for current state at a chosen interval. That makes the operational path familiar: ordinary HTTP authentication, routing, caching, logs, and request timeouts can all apply.
The cost is that the interval creates a freshness bound rather than an immediate update. A short interval increases request volume, including when nothing has changed; a long interval reduces load but leaves the UI stale for longer. Conditional requests or an endpoint that returns a version can reduce unnecessary work when updates are infrequent. Adaptive intervals can also back off when the page is idle and become more frequent when the user is actively watching progress.
Decision rule: Use polling deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
2. Server-Sent Events
SSE provides long-lived server-to-browser event delivery over HTTP with automatic reconnection semantics in EventSource. It fits notifications, progress, and one-way feeds. The server writes events to an open HTTP response; the browser receives them as named or default events and can reconnect when the connection ends.
SSE is not a guarantee that no update can be lost. A reconnecting client still needs a recovery story, such as a replayable event identifier, a last-seen version, or a fresh state request. Authentication, proxy timeouts, connection limits, and cleanup on disconnect also belong in the production design. Keep the stream one-way when that is what the requirement calls for; adding a more general protocol is not automatically an improvement.
Decision rule: Use server-sent events deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
3. WebSockets
WebSockets provide bidirectional messages over a persistent connection. They require connection lifecycle management, an authentication refresh strategy, backpressure decisions, and horizontal fan-out. The ability for either side to send at any time is useful for interactive systems, but it also means that message validation, authorization, rate limits, and failure handling must be designed in both directions.
A connection can close during a deploy, while a browser changes networks, or after credentials expire. Decide whether the client reconnects, how it obtains missed state, and how the server releases subscriptions and other resources. Do not treat an open socket as proof that a user is authorized for every message: authenticate the connection and authorize actions or subscriptions according to the application contract.
Decision rule: Use websockets deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
4. Event identity and ordering
Assign event identifiers or versions so clients can deduplicate, resume, and reject stale updates. “Received later” does not always mean “newer” across distributed producers. Network delay, retries, multiple API instances, and asynchronous work can make messages arrive in an order different from the order in which the underlying state changed.
The identifier must have a defined meaning. It might identify one event, represent a monotonically increasing version for a resource, or act as a cursor in a replayable stream. A client should know whether seeing an identifier twice means “ignore this duplicate,” whether a gap requires a recovery request, and whether a lower version must be rejected. Delivery order without such a contract is only an observation about one connection.
Decision rule: Use event identity and ordering deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
5. Presence
Online/offline presence is approximate because disconnect detection is delayed. Model presence with heartbeats and expiry rather than a permanently trusted boolean. A client can lose power, enter a tunnel, suspend a tab, or disappear without completing a clean disconnect message.
The server can record the last heartbeat and consider the client present only while that timestamp is within an agreed window. That window is a product and operations trade-off: a short expiry marks failures quickly but is more sensitive to temporary network trouble, while a longer expiry reduces false offline transitions but leaves stale presence visible longer. Presence should therefore be described as “last seen recently” or “probably connected,” not as an absolute fact.
Decision rule: Use presence deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
6. Horizontal fan-out
Multiple API instances need shared pub/sub, a broker, or another routing layer so an event created on one instance reaches clients connected to others. With one process, an in-memory subscriber list can appear to work. Once a load balancer distributes connections across instances, that local list knows nothing about clients attached elsewhere.
Separate the concerns: the application creates or observes an event, a shared layer routes it to the relevant instances, and each instance delivers it to its connected clients. You still need to define durability, replay, ordering, duplicate handling, and behavior during broker or instance failure. A queue may be the right tool for durable work processing, while pub/sub may be enough for transient fan-out; the requirement determines which guarantees matter.
Decision rule: Use horizontal fan-out deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.
Worked example
Consider a production full-stack web application with a React client, a Node.js API, persistent storage, authentication, observability, and deployment concerns. Start by writing the requirement in one sentence. For example, “A user viewing a progress dashboard should see completed work within the product’s stated freshness target.” Then list the input and output contracts and identify which of the concepts above owns each failure mode.
The important move is separation: parsing or validation belongs at the boundary; domain rules belong in the domain or service layer; persistence rules belong in the database or repository; presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases much harder to reason about. A transport handler should not quietly become the place where authorization, state transitions, and client rendering assumptions all live.
export async function handleRequest(input: unknown) {
const command = parseCommand(input);
const result = await service.execute(command);
return toHttpResponse(result);
}
This small example intentionally leaves the implementations of parseCommand, service.execute, and toHttpResponse elsewhere. That boundary makes the responsibilities inspectable. The parser handles untrusted input, the service owns the operation, and the response mapper translates the result into the protocol expected by the caller. The same separation is useful when the delivery path changes from polling to SSE: the domain operation and its state contract should not need to be rewritten merely because the notification mechanism changed.
Walk the example with at least four cases: the normal path, an empty or missing value, a duplicate/retry/concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. For a stream, also ask what happens after disconnect and how the client catches up. This is the level of explanation expected in a senior code review or technical interview: name the invariant, locate the owner, and describe the externally visible result rather than stopping at the happy-path code.
Production perspective
Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. Persistent connections add resource concerns that ordinary short requests may hide: open sockets consume memory, timers, file descriptors, and subscription state. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.
When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states. A reconnecting stream commonly needs a stale or recovering state rather than silently presenting old data as current. When it involves security, assume the client can be modified and the network input is untrusted. Authentication of a connection does not remove the need for authorization of the requested resource or action.
Guided lab
Implement a progress dashboard first with polling, then replace only the delivery path with SSE. Document the trade-off that would justify moving to WebSockets, and add reconnection plus event deduplication. Keeping the domain and persistence paths stable is part of the exercise: it lets you compare delivery mechanisms instead of accidentally comparing two unrelated implementations.
Complete the lab with this discipline:
- Write the requirement and two non-requirements.
- List input, output, and error contracts before implementation.
- Implement the smallest correct vertical slice.
- Add at least one invalid-input test and one edge-case test.
- Instrument or inspect the behavior instead of guessing.
- Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
- Explain one alternative design and why you did not choose it.
- Record a short “what would break at 10× scale?” note.
For the polling version, inspect request frequency and the behavior when no progress has changed. For the SSE version, deliberately close the connection and observe the reconnect path. Send or simulate the same event twice and confirm that the client does not apply it twice. Then test an older event arriving after a newer version. The observation is more valuable than merely seeing a green UI: record what the browser, server logs, and metrics show, and connect that evidence to the contract.
Edge cases and failure modes
- Polling: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Also check whether the interval creates excessive request load or leaves the interface unacceptably stale.
- Server-Sent Events: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Include disconnect, reconnect, authentication failure, proxy timeout, and recovery of updates missed during the gap.
- WebSockets: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Include connection close, reconnect storms, unauthorized messages, backpressure, and cleanup of subscriptions.
- Event identity and ordering: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Verify that the identifier or version has a defined scope and that stale updates and gaps produce the intended behavior.
- Presence: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check missed heartbeats, expiry boundaries, temporary network loss, and cleanup after a client disappears without a clean disconnect.
Common mistakes and debugging
- Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
- Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary”
anyvalues. - Testing only the happy path and therefore discovering contracts only after integration.
- Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
- Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.
For debugging, reproduce the smallest failing case, inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch. In a real-time system, inspect the whole path: client state and connection events, browser Network activity, server route or socket logs, broker or queue delivery, and the database version or transaction that produced the event. A message missing from the UI may be a rendering issue, a server authorization decision, a routing gap between instances, a dropped connection, or a correctly rejected stale version.
Check timestamps and event identifiers rather than inferring order from log position alone. Compare the last event the client applied with the server’s current version. If the client reconnects successfully but remains stale, the problem is likely recovery or replay rather than connection establishment. If one API instance sees an event and another does not, investigate the shared fan-out path. If presence flickers, inspect heartbeat intervals, expiry windows, browser suspension, and clock assumptions before changing the UI.
Interview questions
- What problem does Polling solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Server-Sent Events solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does WebSockets solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Event identity and ordering solve, and what trade-off or failure mode would make you choose a different approach?
- What problem does Presence solve, and what trade-off or failure mode would make you choose a different approach?
Answer each question in terms of directionality, freshness, resource cost, reconnection, ordering, and operational behavior. A strong answer also says what guarantee the mechanism does not provide and where the application must add one.
Checkpoint
Without notes, explain Real-Time Delivery: SSE, WebSockets, Polling, Queues, and Presence to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code.
Your explanation should make clear why “real-time” is not a protocol choice by itself. State the freshness requirement, identify who sends messages and who receives them, explain how a reconnecting client recovers, and distinguish approximate presence from authoritative application state. If you cannot describe the recovery path, the design is not finished even if the first connection works.
Mastery checklist
- I can define the core terms precisely.
- I can choose a design from requirements instead of from habit.
- I can implement and test the normal path and edge cases.
- I can explain the runtime, storage, or complexity cost.
- I can identify which layer owns validation, errors, and recovery.
- I can compare at least two reasonable alternatives.
- I can explain how the design changes at larger scale or stricter reliability.
