WebSocket, SSE and Polling Compared Head to Head
Realtime features in Vue.js are more than an open WebSocket. Teams that do not deliberately choose between WebSocket, Server-Sent Events and polling either build too much infrastructure or take on unnecessary network load. This article walks through all three strategies with clean composables, lifecycle handling and concrete recommendations for when to use each.
Table of Contents
- 1. Realtime in Vue.js: three paths, one goal
- 2. Polling: the simplest entry into realtime
- 3. Server-Sent Events: unidirectional push from the server
- 4. WebSocket: bidirectional connections for complex scenarios
- 5. Realtime composables: abstraction and reuse
- 6. Reconnect strategies and connection stability
- 7. Lifecycle integration and avoiding memory leaks
- 8. Managing realtime state in the Vue store
- 9. WebSocket, SSE and polling compared head to head
- 10. Summary
- 11. FAQ
1. Realtime in Vue.js: three paths, one goal
Anyone who wants to display realtime data in Vue.js, whether it is a live dashboard, a chat, notifications or an order status, faces the same fundamental decision: which transport mechanism fits the requirement? The three candidates are polling, Server-Sent Events (SSE) and WebSocket. All three deliver realtime data into Vue components, but they differ fundamentally in protocol, overhead, infrastructure requirements and complexity.
The problem in practice: many teams reach for WebSocket reflexively because it sounds like the most robust solution. Yet WebSocket is often the wrong choice for purely unidirectional server-push scenarios. SSE is simpler to implement, works over plain HTTP infrastructure, and is reconnected natively by the browser. And in scenarios where data changes only rarely, well-designed polling with correct backoff is entirely sufficient. This article walks through all three realtime strategies in Vue.js in detail and gives concrete decision guidance.
2. Polling: the simplest entry into realtime
Polling is the oldest and conceptually simplest realtime strategy in Vue: the client asks the server for new data at regular intervals. The apparent simplicity hides typical pitfalls, though. Naive polling with setInterval and no cleanup leads to memory leaks once the component unmounts. The interval keeps running, keeps firing HTTP requests in the background, and fills memory with closures that never get cleaned up, a classic problem in single-page applications that never shows up in development but causes crashes after hours of production use.
Clean polling in Vue follows a clear pattern: the interval is started in onMounted and must be stopped in onUnmounted. Exponential backoff prevents server errors from turning into a load storm caused by a frontend that keeps polling regardless. Adaptive polling, lengthening the interval when the tab is not visible, cuts network load by up to 90% for inactive tabs. The document.visibilityState event is the key here: visibilitychange fires as soon as the user switches tabs.
// composables/usePolling.js: Adaptive polling with exponential backoff
import { ref, onMounted, onUnmounted } from 'vue'
export function usePolling(fetchFn, options = {}) {
const {
interval = 5000,
maxInterval = 60000,
backoffFactor = 2,
immediate = true,
} = options
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
let timerId = null
let currentInterval = interval
let consecutiveErrors = 0
const poll = async () => {
if (isLoading.value) return
isLoading.value = true
try {
data.value = await fetchFn()
error.value = null
consecutiveErrors = 0
currentInterval = interval // Reset on success
} catch (err) {
error.value = err
consecutiveErrors++
// Exponential backoff: double interval up to maxInterval
currentInterval = Math.min(currentInterval * backoffFactor, maxInterval)
} finally {
isLoading.value = false
}
schedule()
}
const schedule = () => {
// Adaptive: poll less frequently when tab is hidden
const delay = document.hidden ? currentInterval * 4 : currentInterval
timerId = setTimeout(poll, delay)
}
onMounted(() => { if (immediate) poll(); else schedule() })
onUnmounted(() => clearTimeout(timerId))
return { data, error, isLoading }
}
3. Server-Sent Events: unidirectional push from the server
Server-Sent Events are an underrated standard for realtime in Vue. The protocol is about as simple as it gets: the client opens an HTTP connection, the server keeps it open and sends text events in the format data: {...}\n\n. The browser implements automatic reconnect with a configurable retry: field. SSE works over HTTP/1.1 and HTTP/2, needs no special infrastructure, and requires no WebSocket upgrade handshake. Over HTTP/2, multiple SSE connections are even multiplexed over the same TCP connection.
The limitation of SSE: the connection is strictly unidirectional, server to client only. If the application also needs to stream data from the client to the server, SSE is not suitable. For every other scenario, live feeds, progress indicators, notifications, order status updates, SSE is the lighter-weight alternative to WebSocket. In Vue, SSE is abstracted through an EventSource composable that is cleanly tied to the component lifecycle and can manage several event types.
// composables/useSSE.js: Server-Sent Events with typed event handlers
import { ref, onMounted, onUnmounted } from 'vue'
export function useSSE(url, options = {}) {
const { withCredentials = false, events = {} } = options
const isConnected = ref(false)
const lastEvent = ref(null)
const error = ref(null)
let source = null
const connect = () => {
source = new EventSource(url, { withCredentials })
source.onopen = () => { isConnected.value = true; error.value = null }
source.onerror = (e) => {
isConnected.value = false
error.value = e
// Browser automatically reconnects, no manual retry needed
}
// Default message handler
source.onmessage = (e) => {
try { lastEvent.value = JSON.parse(e.data) }
catch { lastEvent.value = e.data }
}
// Register named event listeners
Object.entries(events).forEach(([type, handler]) => {
source.addEventListener(type, (e) => {
try { handler(JSON.parse(e.data)) }
catch { handler(e.data) }
})
})
}
const disconnect = () => {
source?.close()
isConnected.value = false
}
onMounted(connect)
onUnmounted(disconnect)
return { isConnected, lastEvent, error, disconnect, reconnect: connect }
}
4. WebSocket: bidirectional connections for complex scenarios
WebSocket in Vue is the right choice when bidirectional realtime communication is required: chat applications, collaborative editors, multiplayer games, realtime dashboards with user interaction. The WebSocket handshake upgrades an HTTP connection into a persistent TCP-based protocol; after that, client and server can send frames simultaneously without every packet needing a full HTTP request-response cycle. Latency is measurably lower than with HTTP polling, and the overhead per message is minimal.
The complexity lies in lifecycle management. A WebSocket connection in Vue must be closed when the component unmounts, reconnect logic must be implemented, and connection status must be modeled as reactive state. Another critical point: if several components need the same WebSocket channel, the connection should not be reopened in every component but managed in a central Pinia store or a singleton composable. That prevents the same WebSocket URL from being connected fifteen times because fifteen components instantiate the same composable.
// composables/useWebSocket.js: Reactive WebSocket with auto-reconnect
import { ref, shallowRef, onUnmounted } from 'vue'
export function useWebSocket(url, options = {}) {
const { protocols = [], reconnectDelay = 2000, maxReconnects = 10 } = options
const ws = shallowRef(null)
const status = ref('CLOSED') // CONNECTING | OPEN | CLOSING | CLOSED
const lastMessage = ref(null)
const error = ref(null)
let reconnectCount = 0
let reconnectTimer = null
const send = (data) => {
if (ws.value?.readyState === WebSocket.OPEN) {
ws.value.send(typeof data === 'string' ? data : JSON.stringify(data))
}
}
const connect = () => {
if (reconnectCount >= maxReconnects) return
status.value = 'CONNECTING'
const socket = new WebSocket(url, protocols)
socket.onopen = () => {
status.value = 'OPEN'
error.value = null
reconnectCount = 0
}
socket.onmessage = (e) => {
try { lastMessage.value = JSON.parse(e.data) }
catch { lastMessage.value = e.data }
}
socket.onerror = (e) => { error.value = e }
socket.onclose = (e) => {
status.value = 'CLOSED'
if (!e.wasClean && reconnectCount < maxReconnects) {
reconnectCount++
// Exponential backoff for reconnect
reconnectTimer = setTimeout(connect, reconnectDelay * reconnectCount)
}
}
ws.value = socket
}
const disconnect = () => {
clearTimeout(reconnectTimer)
reconnectCount = maxReconnects // Prevent auto-reconnect
ws.value?.close(1000, 'Component unmounted')
}
connect()
onUnmounted(disconnect)
return { status, lastMessage, error, send, disconnect, reconnect: connect }
}
5. Realtime composables: abstraction and reuse
Using raw WebSocket or SSE APIs directly inside components is an antipattern that leads to duplicated code and components that are hard to test. The right pattern for realtime in Vue is to encapsulate the transport completely inside reusable composables. A component that wants to receive order status updates should not need to know whether those updates arrive via WebSocket, SSE or polling; that is an implementation detail that can change without touching a single component.
A well-designed realtime composable returns reactive refs, data, isConnected, error, and exposes actions such as send or reconnect. The connection logic stays entirely inside the composable. When the composable is implemented as a singleton, returning the same instance on a second call instead of creating a new one, several components can share the same data stream without multiple connections being opened. This pattern is identical to the singleton pattern in Pinia stores, just lighter weight.
6. Reconnect strategies and connection stability
Network interruptions are the rule, not the exception, in mobile environments. A realtime implementation in Vue that stays offline permanently after a short connection drop is not production-ready. The baseline reconnect strategy for WebSocket is exponential backoff: the first reconnect attempt after 2 seconds, the second after 4, then 8, 16, up to a configurable maximum of, say, 60 seconds. Without backoff, every client with an interrupted connection bombards the server with reconnect attempts at the same time, causing load spikes.
For SSE, the browser handles automatic reconnect by default. The server can set the reconnect interval via the retry: field in the SSE stream. For polling, backoff is implemented manually inside the composable. In addition to backoff, it is worth watching navigator.onLine and the online event: as soon as the network connection is restored, the app can reconnect immediately instead of waiting for the backoff timer. The interplay of backoff and an online event listener makes reconnect strategies in Vue applications resilient to mobile network switches.
7. Lifecycle integration and avoiding memory leaks
Memory leaks from mismanaged realtime connections in Vue are common and hard to diagnose. The symptom: the application gets slower after hours of use, the browser tab steadily consumes more memory, network requests pile up. The cause: interval timers, EventSource objects or WebSocket instances that were never cleaned up when components unmount. In Vue 3, onUnmounted is the correct place for cleanup. In Vue 2 it was beforeDestroy.
Another subtle leak comes from event listeners on window or document registered inside composables, for example for visibilitychange or online. These must be removed in onUnmounted, otherwise the listeners accumulate with every component instantiation. The correct pattern: store the listener reference in a variable, register it in onMounted, and remove it in onUnmounted using the same reference. Calling addEventListener with an anonymous function makes later removal impossible, a classic leak that is often missed in code review.
8. Managing realtime state in the Vue store
When realtime data in Vue affects several components, for example connection status in the navigation, current data in a list, and a counter in the header, the state belongs in a central Pinia store, not in a local composable. This lets every component access the same reactive state without prop drilling or event buses. The WebSocket or SSE connection is opened once inside the store and writes incoming messages directly into the store state.
The store-composable pattern for realtime in Vue combines both concepts: a Pinia store holds the state, current data, connection status, errors. A composable manages the connection and writes into the store. Initialization happens once in the root component or in the Vue app plugin, not in every individual consuming component. This pattern scales from simple notifications up to complex collaborative features without any architectural changes.
9. WebSocket, SSE and polling compared head to head
| Criterion | Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Client → Server | Server → Client | Bidirectional |
| Overhead | High (full HTTP) | Low (HTTP keepalive) | Minimal (frames) |
| Infrastructure | Any HTTP server | Any HTTP server | WS-capable server needed |
| Reconnect | Manual (backoff) | Automatic in browser | Manual (backoff) |
| Latency | Interval-dependent | Low | Very low |
| Use case | Rarely changing data | Feeds, notifications, progress | Chat, collaboration |
The decision between the three realtime strategies in Vue follows clear logic: if data changes rarely and a delay of several seconds is acceptable, polling with backoff is entirely sufficient. If the server needs to push events and the client only needs to read, SSE is the simpler, infrastructure-light choice. If client and server must exchange data at the same time, chat, collaborative editing, game moves, WebSocket is the only sensible option. Technical complexity increases from polling to WebSocket, and so do infrastructure requirements.
Mironsoft
Vue.js realtime development, architecture consulting and performance optimization
Realtime features that hold up stably in production?
We implement WebSocket, SSE and polling solutions in Vue.js with clean composables, reconnect logic, state management and complete lifecycle handling for your stack.
Architecture review
An audit of existing realtime implementations for memory leaks and reconnect gaps
Composable development
Reusable, testable realtime composables for WebSocket, SSE and adaptive polling
State integration
Pinia store integration for application-wide realtime data without prop drilling
10. Summary
Realtime in Vue.js is not a one-size-fits-all solution but a choice between three transport mechanisms with different tradeoffs. Polling is suitable for rarely changing data with an acceptable delay and only needs adaptive interval management and backoff. Server-Sent Events are the most elegant solution for unidirectional server push: no special infrastructure effort, automatic browser reconnect, low latency. WebSocket is the right choice only where genuinely bidirectional realtime communication is required.
The same Vue principle applies in all three scenarios: transport logic belongs in composables, not in components. onUnmounted cleanup is not optional, it is mandatory. Exponential backoff protects the server from reconnect storms. And anyone who needs to feed several components with the same realtime data should centralize the state in a Pinia store instead of opening multiple parallel connections.
Realtime in Vue.js: the essentials at a glance
Polling
Adaptive interval, exponential backoff, document.hidden for inactive tabs. Cleanup in onUnmounted is mandatory.
SSE
Browser-native reconnect, no WS server needed. Perfect for feeds, notifications and progress indicators.
WebSocket
Bidirectional with minimal frame overhead. Implement reconnect backoff manually. Singleton in the Pinia store for shared connections.
Composables
Keep transport separate from components. Expose reactive refs to the outside. Lifecycle cleanup always in onUnmounted.