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
| Need | Best Approach |
|---|---|
| Server pushes updates, client only reads | SSE |
| Streaming LLM responses | SSE |
| Live dashboard / metrics feed | SSE |
| Progress updates from a background job | SSE |
| Chat / messaging | WebSockets |
| Collaborative editing | WebSockets |
| 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.
| Service | Notes |
|---|---|
| Supabase Realtime | Easy if already on Supabase; subscribe to database changes directly from the client |
| Ably | Full-featured, reliable, excellent SDKs |
| Pusher | Simple, widely used, good free tier |
| Liveblocks | Purpose-built for collaborative editing |
| PartyKit | Edge-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://.