A founder asks: "We need real-time updates."
Half the time, they don't. The other half, WebSockets are overkill for what they actually need. There's a middle: Server-Sent Events and short polling. Either one ships in an afternoon, scales without specialised infrastructure, and covers most of what teams call "real-time."
This post is about that middle ground. What it can do, what it can't, and when WebSockets are genuinely the right answer.
What "real-time" actually means
"Real-time" is a word people use to mean different things. Worth pinning down before picking infrastructure:
Fast eventual consistency. The user did a thing; another user should see the result within a few seconds. Typing notifications, "new message" badges, "someone else commented" toasts. Most products call this real-time. It isn't, technically, but the experience is fine if updates land in 2-5 seconds.
Live updates without refresh. Same as above but with stronger latency expectations: 200ms-1s. Stock tickers, dashboard counters that need to feel alive, collaborative cursors.
Bidirectional low-latency. Both parties need to push updates to each other in tens of milliseconds. Multiplayer games, voice and video, collaborative cursors in Figma. Genuinely needs WebSockets or WebRTC.
If your need is the first kind, you don't need WebSockets. If it's the second kind, you might not. If it's the third, you do.
The polling option
The dumbest possible answer often works: poll the server every few seconds.
useEffect(() => {
const interval = setInterval(() => {
fetch('/api/messages').then(...);
}, 3000);
return () => clearInterval(interval);
}, []);Three seconds of latency on a "new message" indicator is fine for most products. Users don't notice. The cost is one HTTP request every three seconds per active user.
For a product with 1,000 concurrent users, that's 333 requests/second. A Next.js app on Vercel handles this without breaking a sweat. Postgres with a single index on the relevant column handles it without breaking a sweat. Total cost: zero engineering complexity, maybe $20/month of bandwidth.
When polling is right:
- Updates that don't need to be instant.
- User count is under, say, 10,000 concurrent active.
- You're not paying for per-request infra (which Vercel and most platforms aren't, below their tier limits).
- You want a stupid, debuggable, easy-to-reason-about implementation.
The pattern can be smarter than naive polling: increase the interval when the tab is inactive, decrease it when the user is active, back off after errors. But the base case is six lines of code.
The Server-Sent Events option
SSE is HTTP, one-way, server-to-client. The server holds a connection open and pushes data when it has data. The client reconnects automatically when the connection drops.
// Server (Next.js Route Handler)
export async function GET() {
const stream = new ReadableStream({
start(controller) {
const interval = setInterval(() => {
controller.enqueue(`data: ${JSON.stringify({ now: Date.now() })}\n\n`);
}, 1000);
},
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
});
}// Client
useEffect(() => {
const source = new EventSource('/api/events');
source.onmessage = (event) => { ... };
return () => source.close();
}, []);What you get:
- Sub-second push latency.
- Automatic reconnection.
- One connection per client (vs. one request every interval with polling).
- Works through HTTP/2 and HTTP/3 without surprises.
- No new infrastructure. It's just HTTP.
What you don't get:
- Client-to-server push. SSE is one-way. The client still uses normal HTTP requests to send data.
- The browser limit on concurrent connections per origin. With HTTP/1.1, you get 6. With HTTP/2 and HTTP/3, you get many. Use HTTP/2.
SSE is the right answer when:
- You need sub-second push latency for notifications, live counters, dashboard updates.
- The communication is mostly server-to-client (which it is for 80% of "real-time" features).
- You want to scale beyond polling's per-request cost without the WebSocket operational burden.
The catch on serverless platforms: long-lived connections cost money. Vercel charges by execution time, and an SSE connection that stays open for an hour is an hour of execution. For Vercel specifically, route to a long-lived runtime (Edge or a dedicated worker) for SSE endpoints, or push the SSE work to a service designed for it (Cloudflare Workers, Fly, a dedicated Node process).
When WebSockets are actually right
The cases where SSE and polling don't cut it:
1. Bidirectional low-latency. Both client and server need to push updates to each other in under 100ms. Examples: multiplayer game state, voice/video signalling, collaborative editing with cursor positions.
2. High-frequency updates from client. The client is sending more than a few events per second. A polling-based "send" plus an SSE-based "receive" works for low-frequency client events but feels weird above 5-10 events per second.
3. Custom binary protocols. You're not just sending JSON. WebSocket supports binary frames; SSE doesn't.
For these, WebSockets are right. The cost:
- Connection state to manage. WebSocket connections drop. You handle reconnection.
- Infrastructure. Most serverless platforms don't support persistent WebSocket connections out of the box. You'll use a managed service (Liveblocks, Ably, Pusher, PartyKit) or run your own Node process on a long-running server.
- Operations. WebSocket scaling requires sticky sessions or a fan-out service. Either adds complexity.
If your feature really is multiplayer-game-tier real-time, this overhead is justified. If it's "show a notification when a new message arrives," it's not.
A decision tree
Start with polling. Move up only if you have a reason.
- Can I poll every 3-5 seconds? If yes, stop here. Latency is fine. Infrastructure is none.
- Do I need sub-second push latency? If yes, use SSE. Still no specialised infrastructure.
- Do I need client-to-server pushes at high frequency? If yes, use WebSockets. Now you need managed infrastructure or a long-running process.
- Do I need binary frames or sub-100ms bidirectional? If yes, WebSockets are required. Use a managed service unless you have a reason to self-host.
Most projects we ship stop at step 1 or step 2. Step 3+ exists, but it's the exception, not the rule.
A worked example
We shipped a B2B SaaS recently with "real-time" requirements. The team initially asked for WebSockets. We pushed back and asked what specifically needed to be real-time. The list:
- New customer support tickets appearing in the admin queue.
- Notification badge counter updating when new tickets arrive.
- Other admins' "is viewing this ticket" indicator.
For tickets and badges, SSE was overkill. We used 5-second polling. Indistinguishable from real-time at the human scale of B2B support.
For "is viewing this ticket," we used SSE. A single connection per admin, the server pushes presence updates whenever someone opens or closes a ticket. The "users currently viewing" list updates in under a second.
No WebSockets. No new infrastructure. The whole thing runs on the existing Next.js + Postgres stack. We saved an estimated three weeks of work and ongoing operational complexity. The team has shipped two more "real-time" features on the same pattern since.
When you'd come to us
If you're being told "we need real-time" and the team is reaching for WebSockets, it's worth a second opinion. Half the time the answer is polling. A third of the time it's SSE. The last sixth is genuine WebSockets, and we'll build it properly when that's the answer.
See how we work on web apps. We pick infrastructure that fits the actual problem, not the one that sounds most impressive in a status update.