Software Engineering·July 21, 2026·6 min read

WebSockets and Real-Time Communication

WebSockets and Server-Sent Events let your app push data to users without them having to ask for it. This guide explains polling, SSE, and WebSockets — with examples and guidance on when to use each.

websocketsrealtimessenetworkingengineering

Standard HTTP is request-response: the client asks, the server answers, the connection closes. But some features need the server to push data to users without being asked — chat messages, live dashboard updates, collaborative editing, or streaming an AI response token-by-token.

If Cartara flagged this in your diff, you likely added WebSocket connections, Server-Sent Events, or real-time subscription code.


The Three Approaches

Polling

The client repeatedly asks the server for updates at a fixed interval.

```ts
// Check for new messages every 3 seconds
setInterval(async () => {
const messages = await fetch('/api/messages/new').then(r => r.json());
updateUI(messages);
}, 3000);
```

Use when: Updates are infrequent and a few seconds of delay is fine (checking for new emails every minute).

Breaks down when: You have many concurrent users or need low latency. At 1,000 users polling every 3 seconds, that's 333 requests/second — mostly empty "nothing new" responses.

Server-Sent Events (SSE)

The server pushes a stream of events to the client over a long-lived HTTP connection. One direction only: server → client.

```ts
// Server — send events as they happen
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');

const interval = setInterval(() => {
res.write(data: ${JSON.stringify({ update: 'new data' })}\n\n);
}, 1000);

req.on('close', () => clearInterval(interval));
});

// Client — receive events automatically
const source = new EventSource('/api/stream');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
updateUI(data);
};
```

Advantages: Simple, works over standard HTTP, browsers auto-reconnect, no special server setup.

Best for: Live feeds, notifications, streaming AI responses, progress updates, dashboards.

WebSockets

A persistent, two-way connection. Either side can send messages at any time.

```ts
// Client
const ws = new WebSocket('wss://myapp.com/ws');
ws.onopen = () => ws.send(JSON.stringify({ type: 'join', room: 'general' }));
ws.onmessage = (event) => displayMessage(JSON.parse(event.data));

// Send a message from the client
ws.send(JSON.stringify({ type: 'message', text: 'Hello!' }));
```

Advantages: Bidirectional, low overhead per message, low latency.

Best for: Chat, collaborative editing, multiplayer games — anything requiring frequent messages from both client and server.


Choosing the Right Approach

NeedBest Approach
Server pushes updates, client only readsSSE
Streaming LLM responsesSSE
Live dashboard / metrics feedSSE
Progress updates from a background jobSSE
Chat / messagingWebSockets
Collaborative editingWebSockets
Infrequent updates (minutes apart)Polling

Default recommendation: Start with SSE. It covers most real-time use cases and is simpler to implement and debug. Reach for WebSockets when you need frequent bidirectional messages.


Streaming AI Responses

The most common real-time pattern in AI apps is streaming LLM output token-by-token rather than waiting for the full response. This is almost always done with SSE.

```ts
// Next.js API route — stream using Vercel AI SDK
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic('claude-sonnet-4-5'),
messages,
});
return result.toDataStreamResponse(); // streams SSE to client
}

// React client
import { useChat } from 'ai/react';

function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<form onSubmit={handleSubmit}>
{messages.map(m => <div key={m.id}>{m.content}</div>)}
<input value={input} onChange={handleInputChange} />
</form>
);
}
```


Managed Real-Time Services

For most apps, using a managed service is the right call — you avoid running WebSocket servers, managing connection state, and scaling pub/sub infrastructure.

ServiceNotes
Supabase RealtimeEasy if already on Supabase; subscribe to database changes directly from the client
AblyFull-featured, reliable, excellent SDKs
PusherSimple, widely used, good free tier
LiveblocksPurpose-built for collaborative editing
PartyKitEdge-native WebSockets, great for multiplayer

Supabase Realtime is the easiest starting point if you're on Supabase:

```ts
// Subscribe to new messages directly from the client
const channel = supabase
.channel('messages')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => displayMessage(payload.new)
)
.subscribe();
```


Infrastructure Considerations

Serverless incompatibility: Standard serverless functions (Vercel Functions, AWS Lambda) don't support persistent connections. Use a managed service, SSE for short-lived streams, or run WebSocket handling on a long-lived container (Railway, Fly.io).

Multiple servers: WebSocket connections are stateful. If you run multiple servers, you need sticky sessions (so a client always hits the same server) or a pub/sub broker (Redis Pub/Sub) to fan messages across all servers.

Always use wss:// (WebSocket Secure) in production — never plain ws://.


Related concepts

Networking Essentials
What actually happens when a request travels across the internet — DNS, TCP, TLS, HTTP versions, and CDNs explained for builders.
Load Balancing
What a load balancer does, how traffic gets distributed, and what you need to know as a builder using managed platforms.
API Design
How to design APIs that are predictable, consistent, and easy to build on — naming, versioning, errors, pagination, and more.

Turn shipping into understanding

Cartara measures what your team actually learns from every AI coding session.

Join the waitlist