Server-Sent Events (SSE)
SSE provides a unidirectional, server-to-client channel over standard HTTP. It's the simplest way to receive real-time updates.
Try it Online
Open SSE Client — See Server-Sent Events in action!
Overview
| Property | Value |
|---|---|
| Protocol | HTTP (chunked transfer) |
| Direction | Server → Client |
| Latency | 10-50ms |
| Reconnection | Automatic (browser native) |
| Browser Support | All modern (except IE) |
Endpoints
| Endpoint | Description |
|---|---|
GET /sse/:game | Stream by game slug |
GET /sse/type/:type | Stream by game type |
Examples
http://localhost:3000/sse/crash
http://localhost:3000/sse/double
http://localhost:3000/sse/type/multiplier
https://datastream.andrebassi.com.br/sse/crash
Event Format
Event Types
| Event | Description |
|---|---|
initial | First message with latest result |
message | New round result |
| (comment) | Heartbeat (every 15 seconds) |
Event Structure
event: initial
data: {"round_id":512,"game_slug":"crash","extras":"{\"point\": \"11.72\"}"}
: heartbeat 1705432800
event: message
data: {"round_id":513,"game_slug":"crash","extras":"{\"point\": \"3.45\"}"}
Data Format
{
"round_id": 512,
"game_id": 1,
"game_slug": "crash",
"game_type": "multiplier",
"finished_at": "2026-01-17T00:45:42.735266-03:00",
"extras": "{\"point\": \"11.72\"}",
"timestamp": 1768621542123
}
Client Implementation
JavaScript (Browser)
class SSEClient {
constructor(game) {
this.game = game;
this.eventSource = null;
}
connect() {
const url = `/sse/${this.game}`;
this.eventSource = new EventSource(url);
// Initial event (last result)
this.eventSource.addEventListener('initial', (e) => {
const data = JSON.parse(e.data);
console.log('Initial result:', data);
this.handleMessage(data);
});
// New messages
this.eventSource.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
console.log('New round:', data);
this.handleMessage(data);
});
// Connection opened
this.eventSource.onopen = () => {
console.log('SSE connected');
};
// Error handling (reconnection is automatic)
this.eventSource.onerror = (e) => {
console.log('SSE error, reconnecting...');
};
}
handleMessage(data) {
const extras = JSON.parse(data.extras);
console.log(`Round ${data.round_id}: ${JSON.stringify(extras)}`);
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
}
}
}
// Usage
const client = new SSEClient('crash');
client.connect();
React Hook
import { useState, useEffect } from 'react';
function useSSE(game) {
const [data, setData] = useState(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource(`/sse/${game}`);
eventSource.addEventListener('initial', (e) => {
setData(JSON.parse(e.data));
setConnected(true);
});
eventSource.addEventListener('message', (e) => {
setData(JSON.parse(e.data));
});
eventSource.onerror = () => {
setConnected(false);
};
return () => {
eventSource.close();
};
}, [game]);
return { data, connected };
}
// Usage
function GameCard({ game }) {
const { data, connected } = useSSE(game);
if (!data) return <div>Loading...</div>;
return (
<div>
<h2>{data.game_slug}</h2>
<p>Round: {data.round_id}</p>
<p>Status: {connected ? 'Connected' : 'Reconnecting...'}</p>
</div>
);
}
curl Testing
# Connect to SSE stream
curl -N http://localhost:3000/sse/crash
# Output:
# event: initial
# data: {"round_id":512,"game_slug":"crash",...}
#
# : heartbeat 1705432800
#
# event: message
# data: {"round_id":513,"game_slug":"crash",...}
Go Client
package main
import (
"bufio"
"encoding/json"
"log"
"net/http"
"strings"
)
func main() {
resp, err := http.Get("http://localhost:3000/sse/crash")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
// Skip comments and empty lines
if strings.HasPrefix(line, ":") || line == "" {
continue
}
// Parse event type
if strings.HasPrefix(line, "event:") {
eventType := strings.TrimPrefix(line, "event: ")
log.Println("Event type:", eventType)
continue
}
// Parse data
if strings.HasPrefix(line, "data:") {
data := strings.TrimPrefix(line, "data: ")
var round map[string]interface{}
json.Unmarshal([]byte(data), &round)
log.Printf("Round %v: %v", round["round_id"], round["extras"])
}
}
}
Server Implementation
// internal/adapters/inbound/sse/handlers.go
func (h *Handlers) StreamByGame(c *fiber.Ctx) error {
game := c.Params("game")
ctx := c.Context()
c.Set("Content-Type", "text/event-stream")
c.Set("Cache-Control", "no-cache")
c.Set("Connection", "keep-alive")
c.Set("Transfer-Encoding", "chunked")
c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
// Send initial data
if latest, err := h.roundRepo.GetLatest(ctx, game); err == nil {
data, _ := json.Marshal(latest)
fmt.Fprintf(w, "event: initial\ndata: %s\n\n", data)
w.Flush()
}
// Subscribe to Redis Pub/Sub
channel := fmt.Sprintf("stream:%s", game)
sub, _ := h.subscriber.Subscribe(ctx, channel)
// Heartbeat ticker
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for {
select {
case round := <-sub:
data, _ := json.Marshal(round)
fmt.Fprintf(w, "event: message\ndata: %s\n\n", data)
w.Flush()
case <-ticker.C:
fmt.Fprintf(w, ": heartbeat %d\n\n", time.Now().Unix())
w.Flush()
case <-ctx.Done():
return
}
}
}))
return nil
}
Best Practices
Connection Management
- Let the browser handle reconnection - SSE auto-reconnects on disconnect
- Use
Last-Event-ID- For resuming from last received event - Implement heartbeats - Keep connection alive through proxies
Message Handling
- Use named events - Differentiate message types
- Keep payloads small - SSE is text-only
- Parse errors gracefully - Handle malformed JSON
Performance
- Use HTTP/2 - Multiple streams over single connection
- Enable compression - gzip for text data
- Set appropriate timeouts - Prevent zombie connections
Advantages of SSE
| Advantage | Description |
|---|---|
| Simplicity | Just HTTP, no special protocol |
| Auto-reconnect | Browser handles reconnection |
| Event ID | Resume from last event |
| Proxy-friendly | Works through HTTP proxies |
| Text-based | Easy to debug with curl |
Limitations
| Limitation | Workaround |
|---|---|
| Server → Client only | Use REST API for client → server |
| Text only | Base64 encode binary data |
| 6 connections per domain | Use HTTP/2 or single multiplexed stream |
| No IE support | Use polyfill or WebSocket fallback |
Troubleshooting
Connection Keeps Dropping
Cause: Proxy or load balancer timeout
Solutions:
- Implement heartbeats (every 15s)
- Configure proxy timeouts
- Use HTTP/2
No Events Received
Solutions:
- Check Redis Pub/Sub is working
- Verify consumer is running
- Check game slug is valid
- Look at server logs
High Memory Usage
Cause: Too many open connections
Solutions:
- Implement connection pooling
- Use HTTP/2 multiplexing
- Set max connection limits
Comparison with WebSocket
| Feature | SSE | WebSocket |
|---|---|---|
| Direction | Server → Client | Bidirectional |
| Protocol | HTTP | WebSocket |
| Auto-reconnect | Yes | No |
| Binary data | No | Yes |
| Browser support | IE except | Universal |
| Proxy-friendly | Yes | Sometimes |
| Debug with curl | Yes | No |
When to Use SSE
Use SSE when:
- Only server needs to send data
- Auto-reconnection is important
- Behind proxies or firewalls
- Simple debugging is valuable
Consider WebSocket when:
- Client needs to send data
- Binary data is needed
- Lower latency is critical