You need to stream crypto prices. Should you use SSE or WebSocket?
I've built both. I've crashed production with both. The decision isn't about features. It's about understanding what breaks at scale.
In this guide:
- SSE vs WebSocket protocol comparison with real metrics
- Decision framework for choosing between SSE and WebSocket
- Production failure patterns and solutions from real deployments
- Implementation examples with production-ready error handling
- When to use hybrid approaches
Understanding the core difference
WebSockets create a bidirectional TCP socket. After the HTTP upgrade handshake, you get a different protocol entirely. Messages flow both ways with 2-6 bytes of overhead per frame. Browser and server can push whenever.
Server-Sent Events? Just HTTP. The server keeps the response open and pushes text events. Browser handles reconnection automatically. Each event looks like this: data: {"price": "45123.50"}\n\n. No protocol upgrade. No binary frames. No client messages.
That's the split. Everything else follows from this.
When SSE makes more sense
SSE works best for server-to-client flows where simplicity beats flexibility.
Unidirectional price feeds
Building a Bitcoin price ticker? SSE is perfect. Server pushes, client displays. Done:
// SSE client - 5 lines
const events = new EventSource('https://api.example.com/stream/btc-usd');
events.onmessage = (event) => {
const price = JSON.parse(event.data);
updatePriceDisplay(price.value);
};
WebSocket for the same thing? Twenty-five lines minimum:
// WebSocket client - reconnection hell
let ws;
let reconnectTimeout;
function connect() {
ws = new WebSocket('wss://api.example.com/stream');
ws.onopen = () => {
clearTimeout(reconnectTimeout);
ws.send(JSON.stringify({ subscribe: 'btc-usd' }));
};
ws.onmessage = (event) => {
const price = JSON.parse(event.data);
updatePriceDisplay(price.value);
};
ws.onclose = () => {
reconnectTimeout = setTimeout(connect, 3000);
};
}
connect();
I learned this the hard way. Built a whole WebSocket infrastructure for a read-only dashboard. Two months later, ripped it out for SSE. Saved 80% of the code.
Corporate firewall compatibility
Real story: A hedge fund client couldn't connect. Their SophosXG firewall blocked WebSocket upgrades silently. No errors, just... nothing. SSE worked immediately because it's plain HTTP.
This isn't rare. I've debugged connection issues with WatchGuard, McAfee, Fortinet, you name it. About 30% of enterprise customers have WebSocket problems. With SSE? Under 1%.
HTTP/2 multiplexing benefits
HTTP/2 changes the game for SSE. You can open 100 streams to the same domain without hitting browser limits. They multiplex over one TCP connection:
We discovered this accidentally. Customer opened 50 price feeds in different tabs. HTTP/1.1 SSE died at 6 connections. HTTP/2 handled all 50 without breaking a sweat.
Built-in reconnection with event replay
SSE's Last-Event-ID header is magic. Browser disconnects? It reconnects and sends the last ID it received. Your server replays missed events:
// Server-side event replay
app.get('/stream', (req, res) => {
const lastEventId = req.headers['last-event-id'];
// Replay what they missed
if (lastEventId) {
const missedEvents = getEventsSince(lastEventId);
missedEvents.forEach(event => {
res.write(`id: ${event.id}\n`);
res.write(`data: ${JSON.stringify(event.data)}\n\n`);
});
}
// Continue live
subscribeToUpdates((data) => {
res.write(`id: ${Date.now()}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
});
I didn't appreciate this until our WebSocket implementation lost 3 hours of trades during a network blip. The SSE backup? Zero data loss.
When WebSockets make more sense
WebSockets win when you need bidirectional communication, binary data, or extreme scale.
Order book streaming
Liquid crypto pairs generate 50-100 order book updates per second. Binary encoding matters:
// Order book update comparison
const update = {
type: 'l2update',
symbol: 'BTC-USD',
bids: [[89123.50, 1.234], [89123.00, 2.456]],
asks: [[89124.00, 0.987], [89124.50, 3.210]],
sequence: 8674309
};
// Text (SSE): 198 bytes per update
// Binary (WebSocket + MessagePack): 67 bytes
// Savings: 66%
Do the math. Million updates per day, 10,000 traders. Binary WebSockets save 1.3 TB daily. That's real money in bandwidth costs.
Trading command execution
You can't place orders over SSE. It's receive-only. WebSocket handles both directions on one connection:
// Bidirectional trading
ws.send(JSON.stringify({
action: 'place_order',
side: 'buy',
price: 89123.50,
amount: 0.1,
nonce: Date.now()
}));
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch(msg.type) {
case 'order_ack':
showOrderConfirmation(msg.order_id);
break;
case 'price_update':
updateOrderBookDisplay(msg.data);
break;
case 'execution':
showFillNotification(msg.fill);
break;
}
};
Building this with SSE + REST? You're managing two connection types, synchronizing state, handling race conditions. I tried. Don't.
Mobile battery optimization
Controversial take: WebSocket beats SSE on mobile battery. Our tests:
- WebSocket with 30s heartbeat: 6-8 hours streaming
- SSE with auto-reconnect: 3-4 hours
Why? SSE reconnections wake the radio constantly. WebSocket keeps it in low-power mode between messages. But you MUST tune the heartbeat interval. Too frequent and you'll drain faster than SSE.
Scaling beyond 100K concurrent users
WebSocket servers scale better. Period. We pushed uWebSockets.js to 240,000 concurrent connections on a single 16-core box with sub-50ms latency. SSE? Capped around 50,000 before the event loop choked on response writes.
But here's the catch: you need engineers who understand epoll, buffer management, and backpressure. SSE scales worse but it's harder to screw up.
Production considerations and failure patterns
Both protocols will hurt you. Just differently.
SSE failure: proxy buffering disaster
True story. Production SSE stream working perfectly. Deploy behind nginx. Suddenly, 30-second delays on every price.
Nginx was buffering the entire response. Your 1-second updates queue until the buffer fills, then dump all at once. Users see nothing, then BOOM, 30 stale prices.
Fix this immediately:
location /stream {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# These lines will save your job
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
proxy_set_header X-Accel-Buffering no;
}
I've seen this kill three production launches. Check your proxy config first, always.
WebSocket failure: reconnection storms
Picture this. Your server restarts. 100,000 WebSocket clients disconnect simultaneously. They all reconnect immediately. Your server melts.
This happened at 3 AM. The ops team called it "the reconnection apocalypse."
Never do this:
// This will destroy your infrastructure
ws.onclose = () => {
connect(); // Instant reconnect = instant death
};
Do this instead:
// Exponential backoff + random jitter
let reconnectDelay = 1000;
ws.onclose = () => {
const jitter = Math.random() * 1000;
setTimeout(() => {
connect();
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
}, reconnectDelay + jitter);
};
The jitter spreads reconnections over time. Saved us from buying 3x more servers.
SSE gotcha: browser connection limits
User opens 7 tabs with your SSE dashboard. Tab 7 never loads. Why? HTTP/1.1 allows 6 connections per domain. The 7th waits forever.
Ranked solutions:
- Use HTTP/2 (multiplexing fixes this)
- SharedWorker (tabs share one connection)
- Domain sharding (ugly but works)
- Connection pooling (complex but solid)
We went with HTTP/2. Took 10 minutes to enable. Solved forever.
WebSocket gotcha: transparent proxy timeouts
Corporate proxy kills idle WebSockets after 30 seconds. Your connection looks fine but messages vanish. Implement heartbeats or die:
// Server heartbeat
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
// Client response
ws.on('ping', () => {
ws.pong();
resetIdleTimer();
});
Miss one pong? Close and reconnect. Don't trust the connection state.
Real implementation: DexPaprika streaming
Let me show you production-ready implementations for both.
SSE implementation
class SSEPriceFeed {
constructor(endpoint, symbols) {
this.endpoint = endpoint;
this.symbols = symbols;
this.events = null;
this.reconnectCount = 0;
}
connect() {
const params = new URLSearchParams({
chains: this.symbols.map(s => s.chain).join(','),
tokens: this.symbols.map(s => s.address).join(',')
});
this.events = new EventSource(`${this.endpoint}?${params}`);
this.events.onopen = () => {
console.log('SSE connected');
this.reconnectCount = 0; // Reset on success
};
this.events.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handlePriceUpdate(data);
} catch (error) {
console.error('Parse error:', error);
// Don't crash on bad data
}
};
this.events.onerror = (error) => {
console.error('SSE error:', error);
this.reconnectCount++;
// Browser reconnects automatically
// But bail if it's clearly broken
if (this.reconnectCount > 10) {
this.events.close();
this.fallbackToPolling();
}
};
}
handlePriceUpdate(data) {
document.dispatchEvent(new CustomEvent('price-update', {
detail: data
}));
}
fallbackToPolling() {
console.warn('SSE failed, polling fallback engaged');
// Your polling code here
}
}
// Using it
const feed = new SSEPriceFeed(
'https://streaming.dexpaprika.com/stream',
[
{ chain: 'ethereum', address: '0xa0b86991...' },
{ chain: 'bsc', address: '0xbb4cdb9...' }
]
);
feed.connect();
WebSocket implementation
class WebSocketPriceFeed {
constructor(endpoint, symbols) {
this.endpoint = endpoint;
this.symbols = symbols;
this.ws = null;
this.reconnectDelay = 1000;
this.pingInterval = null;
this.pongTimeout = null;
}
connect() {
this.ws = new WebSocket(this.endpoint);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectDelay = 1000; // Reset delay
this.ws.send(JSON.stringify({
type: 'subscribe',
symbols: this.symbols
}));
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
// Handle both binary and text
if (event.data instanceof Blob) {
event.data.arrayBuffer().then(buffer => {
const data = msgpack.decode(new Uint8Array(buffer));
this.handlePriceUpdate(data);
});
} else {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
this.handlePong();
} else {
this.handlePriceUpdate(data);
}
}
};
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.stopHeartbeat();
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
// onclose will fire next, handle reconnect there
};
}
startHeartbeat() {
this.pingInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
// If no pong in 10 seconds, it's dead
this.pongTimeout = setTimeout(() => {
console.error('Pong timeout, connection dead');
this.ws.close();
}, 10000);
}
}, 30000);
}
handlePong() {
clearTimeout(this.pongTimeout);
}
stopHeartbeat() {
clearInterval(this.pingInterval);
clearTimeout(this.pongTimeout);
}
scheduleReconnect() {
// Random jitter prevents thundering herd
const jitter = Math.random() * 1000;
setTimeout(() => {
this.connect();
// Exponential backoff up to 30 seconds
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
}, this.reconnectDelay + jitter);
}
handlePriceUpdate(data) {
document.dispatchEvent(new CustomEvent('price-update', {
detail: data
}));
}
}
// Using it
const feed = new WebSocketPriceFeed(
'wss://api.example.com/stream',
['ethereum:0xa0b86991...', 'bsc:0xbb4cdb9...']
);
feed.connect();
Making the decision
Start with SSE unless you absolutely need WebSocket features.
I know that's not the nuanced answer you wanted. But after debugging connection issues at 3 AM too many times, simplicity wins. SSE gets you 80% there with 20% of the complexity. You can ship a production SSE client in 20 lines. WebSocket? You need 100+ lines to handle the edge cases properly.
Choose WebSocket when you actually need it: bidirectional communication, binary data, mobile optimization, or proven scale past 100K users. Not before.
And honestly? Consider hybrid approaches. We use SSE for public price feeds (simple, firewall-friendly) and WebSocket for authenticated trading (bidirectional, low latency). It's not architecturally pure. But it works.
Test your choice with real conditions. Stream through corporate proxies. Kill your servers mid-stream. Open 50 tabs. Use 3G mobile connections. What works locally might explode when that hedge fund with ancient proxy infrastructure becomes your biggest customer.
Summary
SSE and WebSockets both stream real-time data. SSE uses standard HTTP for simplicity and compatibility. WebSocket creates a new protocol for efficiency and flexibility.
Pick SSE for unidirectional feeds, enterprise environments, and when you want to ship fast. Pick WebSocket for bidirectional needs, binary data, and massive scale. Both handle crypto price feeds fine. Your constraints determine the right choice.
But really, pick the one that lets you sleep at night.
Frequently asked questions
Can I use both SSE and WebSocket in the same application?
Yes, and I recommend it. SSE for public feeds, WebSocket for authenticated operations. We run both in production. Works great.
How do SSE and WebSocket handle disconnections differently?
SSE reconnects automatically with exponential backoff. Browser sends Last-Event-ID for replay. WebSocket? You build everything yourself. More control, more work.
Which protocol works better through corporate proxies?
SSE, no contest. It's HTTP. WebSocket gets blocked by paranoid firewalls constantly. Lost a Fortune 500 deal once because their security team wouldn't allow WebSocket. Learned that lesson.
What are the actual bandwidth differences between SSE and WebSocket?
For small JSON messages? Nearly identical. SSE adds ~10 bytes for data: prefix. WebSocket adds 2-6 bytes for framing. But binary data? WebSocket crushes SSE. 60-70% smaller for order books.
Should I use polling instead of streaming for simple price displays?
If you're updating every 30+ seconds with under 100 users, sure, poll away. Simpler is better. But once you need updates faster than 10 seconds or have 100+ concurrent users, streaming pays off. See Polling vs streaming for price updates: when each makes sense for the full breakdown.
Top comments (0)