Siddhant DevalAuthor
Senior Full-Stack Engineer·Aug 27, 2026·12 min read
Real-time & Persistent State: WebSockets, SSE, BroadcastChannel & IndexedDB
Pull-based server state (React Query) and push-based server state (WebSockets, SSE) are architecturally distinct domains. This article covers the third state domain: real-time event streams, the merge-vs-invalidate decision, cross-tab synchronization with BroadcastChannel, and local persistence with IndexedDB and Zustand persist middleware.
Technical Series
Frontend State Architecture
Part 7 of 8
Real-time & Persistent State: WebSockets, SSE, BroadcastChannel & IndexedDB
The series has so far covered two state domains: UI state (Parts 1–3, 6) and pull-based server state (Part 4). Both assume a request-response model: the UI asks, the server answers.
But a large and growing class of modern applications lives outside that model: chat, collaborative editing, live dashboards, trading feeds, notifications, online presence indicators. For these, the server pushes updates without being asked. This is push-based server state — the third domain, with its own architecture, its own failure modes, and its own set of tools.
This article also covers local persistence — the browser's storage layer — and cross-tab synchronization, which becomes critical when push-based state changes in one tab and must propagate to others.
1. Pull vs. Push: The Architectural Divide
| Pull-Based | Push-Based | |
|---|---|---|
| Initiation | Client fetches on demand | Server sends when data changes |
| Caching model | Stale-while-revalidate, invalidation | Event stream, append or replace |
| React Query fit | ✅ Natural — polling, caching, refetching | ⚠️ Awkward — requires manual integration |
| Protocol | HTTP request-response | WebSocket, SSE, WebRTC |
| State merge logic | Invalidate and refetch | Merge incoming event into local state |
| Primary challenge | Staleness, waterfall | Connection lifecycle, reconnect, ordering |

Expand
Mental Model Check
Think of pull-based state as a snapshot that ages (solved by React Query's stale-while-revalidate). Think of push-based state as a stream of events that must be applied to a local state model (solved by WebSocket/SSE + a merge strategy). The two require different architectural thinking.
2. WebSocket Connection as a State Machine
A WebSocket connection has a native lifecycle with four states:
CONNECTING (0) → OPEN (1) → CLOSING (2) → CLOSED (3)
Model this as a state machine — not as a bag of booleans:
typescript

Expand
3. Server-Sent Events (SSE): Simpler Unidirectional Push
SSE is often the correct choice over WebSockets when data flows only from server to client. Its advantages:
- HTTP/2 multiplexed — no separate TCP connection needed
- Automatic reconnect built into the browser's
EventSourceAPI - Passes through proxies and CDNs without special configuration (no WebSocket upgrade)
- Text-based, human-readable — easier to debug
typescript
Pro Tip & Optimization
SSE vs. WebSocket decision rule: If data flows only server → client (notifications, live prices, progress updates), use SSE — simpler, more reliable through infrastructure. If the client needs to send data continuously too (chat, collaborative editing, gaming), use WebSockets.
4. The Merge vs. Invalidate Decision
When an incoming server event arrives, you face a fundamental architectural choice:
Merge: Apply the event to the local state immediately.
Invalidate: Mark the cached data as stale and refetch from the server.
typescript
Merge is correct when:
- The incoming event is the state change (a new chat message, a new log entry)
- You can apply the event locally without round-tripping to the server
- Out-of-order delivery would be tolerable or handled client-side
Invalidate is correct when:
- The event is a signal that something changed, not the data itself
- Multiple concurrent users might have changed the same field (inventory, votes)
- Local merge logic would be complex or error-prone
- Consistency with server state is more important than immediate update speed
5. BroadcastChannel: Cross-Tab State Sync
When a user has your application open in multiple tabs, push-based state changes in one tab are invisible to the others.
BroadcastChannel bridges this:typescript
BroadcastChannel works across tabs of the same origin (same protocol, domain, and port). It does not work across different origins or across devices.Alternative for broader support: The
storage event fires in all tabs when localStorage is modified by another tab:typescript
Architectural Note
The
storage event does NOT fire in the tab that made the change — only in other tabs. This is intentional: the modifying tab already has the latest value in memory.
Expand
6. Local Persistence as a State Layer
The browser provides four storage mechanisms for persisting state across page loads:
| Store | Capacity | Scope | Sync/Async | XSS accessible | Use case |
|---|---|---|---|---|---|
localStorage | ~5–10 MB | Origin, all tabs | Synchronous | ✅ Yes (dangerous) | Non-sensitive UI preferences |
sessionStorage | ~5 MB | Origin, this tab only | Synchronous | ✅ Yes | Tab-scoped temporary state |
IndexedDB | Hundreds of MB | Origin | Asynchronous | ✅ Yes | Offline data, drafts, large structured data |
Cookie (httpOnly) | ~4 KB | Configurable | Via HTTP header | ❌ No | Auth tokens, session IDs |
Performance / Safety Warning
Never store auth tokens, API keys, or secrets in
localStorage or sessionStorage. Any injected script — via XSS — has full read access to these stores via localStorage.getItem(). Auth tokens belong in httpOnly cookies, which are inaccessible to JavaScript entirely.6.1 Zustand Persist Middleware
For non-sensitive UI state (theme preference, sidebar open/closed, draft content), Zustand's persist middleware synchronizes store state to
localStorage automatically:typescript
Crucial Requirement
Always include a
version field in your persist config. When you change the shape of persisted state (rename a field, change a type), increment the version and write a migrate function. Without this, old persisted data with a different shape will silently corrupt the store on load.7. Offline-First: Queue, Sync, Replay
For applications that need to work without a network connection:
typescript
8. References
Research & Synthesis Note
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.
#React#WebSockets#SSE#BroadcastChannel#IndexedDB#Real-time#Offline-first#Zustand#State Management