WebSocket vs SSE vs Polling: A Frontend Developer’s Guide to Real-Time Updates
DEV Community

WebSocket vs SSE vs Polling: A Frontend Developer’s Guide to Real-Time Updates

When we need "real-time" data in a frontend application, WebSocket is usually the first thing that comes to mind. But real-time does not automatically mean WebSocket. Sometimes polling is enough. Sometimes Server-Sent Events (SSE) are a much simpler solution. And sometimes you really do need a persistent, bidirectional connection. From a frontend perspective, the useful question is not: Which one is more powerful? It is: How does data need to flow between my frontend and backend? Let's look at Polling, SSE and WebSocket from that perspective. The mental model Polling The client keeps asking the server for new data. Client → Server: Anything new? Server → Client: Not yet. Client → Server: Anything new? Server → Client: Yes. Server-Sent Events The client opens a connection once, and the server keeps pushing updates. Client → Server: Open stream Server → Client: Update Server → Client: Update Server → Client: Update WebSocket Both sides can send messages whenever they want. Client ⇄ Server That difference already answers a large part of the decision. 1. Polling Polling is the simplest approach. The frontend sends a normal HTTP request every few seconds to check whether something has changed. Imagine the user starts a background job and the backend gives us: GET /api/jobs/42 which returns: { "id": "42", "status": "processing", "progress": 60 } A few seconds later: { "id": "42", "status": "completed", "progress": 100 } In React, we can simply keep requesting the endpoint until the job finishes. useEffect(() => { let timeoutId: ReturnType ; let cancelled = false; async function poll() { const response = await fetch(/api/jobs/${jobId}); const job = await response.json(); if (cancelled) return; setJob(job); if (job.status === "queued" || job.status === "processing") { timeoutId = setTimeout(poll, 2000); } } poll(); return () => { cancelled = true; clearTimeout(timeoutId); }; }, [jobId]); I prefer setTimeout here instead of setInterval because the next request is scheduled after the previous one finishes, so slow requests do not start piling up. In a real application, a server-state library can make this much cleaner. For example, TanStack Query supports polling through refetchInterval : useQuery({ queryKey: ["job", jobId], queryFn: () => fetchJob(jobId), refetchInterval: (query) => { const status = query.state.data?.status; return status === "completed" || status === "failed" ? false : 2000; }, }); The important point is that TanStack Query is not a real-time transport here. It is still making normal HTTP requests repeatedly. When should you use polling? Polling is a good fit when: - updates are relatively infrequent - a few seconds of delay are acceptable - the backend already exposes a normal HTTP endpoint - simplicity is more important than instant updates Common examples: - background jobs - payment status - export generation - deployment status - dashboards refreshing every few seconds The downside is obvious at scale. If thousands of users poll every two seconds, most requests may return exactly the same data. You are still paying for the request even when nothing changed. 2. Server-Sent Events (SSE) SSE changes the model. Instead of repeatedly asking for updates, the frontend opens an HTTP connection and the server keeps that response open. For example, the backend might expose: GET /api/orders/42/events with: Content-Type: text/event-stream and send events like: event: order.updated data: {"id":"42","status":"processing"} event: order.updated data: {"id":"42","status":"shipped"} The browser already provides an API for consuming this: useEffect(() => { const source = new EventSource(/api/orders/${orderId}/events); source.addEventListener("order.updated", (event) => { const updatedOrder = JSON.parse((event as MessageEvent).data); setOrder(updatedOrder); }); return () => source.close(); }, [orderId]); Unlike polling, the server can push an update immediately when something changes. And unlike the native WebSocket API, EventSource also reconnects automatically when the connection is interrupted. But SSE is one-way, right? Yes. The stream itself is: Server → Client But your application can still send data with normal HTTP requests. For example: GET /api/chat/events ↓ Receive messages through SSE POST /api/chat/messages ↑ Send messages through HTTP So HTTP + SSE can be a perfectly valid architecture. One thing to know about EventSource The native EventSource API is intentionally simple. For example, it does not let you attach arbitrary headers such as: Authorization: Bearer ... If you need more control over the request, a fetch-based SSE client such as @microsoft/fetch-event-source can be useful. When should you use SSE? SSE is a strong choice when the frontend mostly needs to listen. Examples: - notifications - live dashboards - order status - log streaming - progress updates - live feeds - AI response streaming A useful question is: Does my frontend mostly receive updates from the server? If yes, SSE is worth considering before WebSocket. 3. WebSocket Now imagine something more interactive. For example, a chat application: Client → Server: typing.started Server → Client: user.online Client → Server: message.send Server → Client: message.created Both sides need to communicate frequently. This is where WebSocket fits naturally. The backend might expose: wss://api.example.com/realtime and send messages such as: { "type": "message.created", "payload": { "id": "123", "text": "Hello", "userId": "42" } } From the frontend, the browser already provides a native API: useEffect(() => { const socket = new WebSocket("wss://api.example.com/realtime"); socket.onopen = () => { console.log("Connected"); }; socket.onmessage = (event) => { const message = JSON.parse(event.data); console.log(message); }; return () => socket.close(); }, []); And sending a message works over the same connection: socket.send( JSON.stringify({ type: "message.send", payload: { text: "Hello!", }, }) ); That's the biggest difference. With WebSocket, communication is truly bidirectional: Client ⇄ Server But opening a WebSocket is the easy part. In production, you may also need to handle things like: - reconnecting - authentication - heartbeats - re-subscribing after reconnect - duplicate or missed messages - connection state For simple applications, the native API may be enough. For larger React applications, libraries like react-use-websocket can help manage the connection lifecycle. What about Socket.IO? Socket.IO often gets mentioned together with WebSocket, but they are not the same thing. WebSocket is a protocol and browser API. Socket.IO is a higher-level client/server library that adds features such as: - automatic reconnection - named events - acknowledgements - rooms - transport management One important detail: A Socket.IO client expects a Socket.IO server. You cannot connect socket.io-client directly to an arbitrary plain WebSocket backend. Quick comparison A practical way to choose Imagine you're building an order tracking page: created ↓ paid ↓ processing ↓ shipped ↓ delivered If checking every 30 seconds is fine: Polling is probably enough. If the backend should notify the UI immediately: SSE is a natural fit. Now add: - live chat - typing indicators - online presence - frequent client/server events Now the communication becomes genuinely bidirectional. That's where WebSocket starts to make much more sense. Final rule of thumb Updates can wait? → Polling Server mainly pushes updates to the client? → SSE Both sides communicate frequently? → WebSocket There are more things to consider in production (authentication, infrastructure, proxies, scalability and delivery guarantees), but this is a good starting point from the frontend side. And most importantly: Don't choose WebSocket just because the feature is called "real-time." Choose the simplest communication model that matches the actual data flow of your application. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.