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.
Open SSE Client — See Server-Sent Events in action!
Overview
| Property | Value |
|---|---|
| Protocol | HTTP (chunked transfer) |
| Direction | Server → Client |
| Latency | ~50ms |
| Reconnection | Automatic (browser native) |
| Browser Support | All modern (except IE) |
Data Flow
SSE receives data through the same pipeline as WebSocket, but with HTTP chunked transfer:
| Step | Component | Action | Latency |
|---|---|---|---|
| 1 | Postgres | INSERT INTO rounds | - |
| 2 | Debezium | Reads WAL, publishes to NATS | ~1-5ms |
| 3 | NATS | Delivers to Consumer | <1ms |
| 4 | Consumer | Processes, publishes to Redis | <1ms |
| 5 | Redis Pub/Sub | Delivers to Backend | <1ms |
| 6 | Backend | HTTP chunked response | ~10-50ms |
| 7 | Browser | EventSource API | - |
Total end-to-end latency: ~50ms
SSE uses HTTP chunked transfer which adds overhead compared to WebSocket frames. However, it provides automatic reconnection and works through all HTTP proxies.
Authentication
SSE uses query parameter for authentication (headers not supported by EventSource API):
const apiKey = 'sk_live_xxxxx';
const sse = new EventSource(`/sse/crash?api_key=${encodeURIComponent(apiKey)}`);
See the Authentication Guide for detailed information on creating and managing API keys.
Endpoints
| Endpoint | Description |
|---|---|
GET /sse/:game?api_key=xxx | Stream by game slug |
GET /sse/type/:type?api_key=xxx | Stream by game type |
Examples
http://localhost:3000/sse/crash?api_key=sk_live_xxxxx
http://localhost:3000/sse/double?api_key=sk_live_xxxxx
http://localhost:3000/sse/type/multiplier?api_key=sk_live_xxxxx
https://datastream.hypetech.games/sse/crash?api_key=sk_live_xxxxx
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, apiKey) {
this.game = game;
this.apiKey = apiKey;
this.eventSource = null;
}
connect() {
const url = `/sse/${this.game}?api_key=${encodeURIComponent(this.apiKey)}`;
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', 'sk_live_xxxxx');
client.connect();
React Hook
import { useState, useEffect } from 'react';
function useSSE(game, apiKey) {
const [data, setData] = useState(null);
const [connected, setConnected] = useState(false);
useEffect(() => {
const eventSource = new EventSource(`/sse/${game}?api_key=${encodeURIComponent(apiKey)}`);
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 with API key
curl -N "http://localhost:3000/sse/crash?api_key=sk_live_xxxxx"
# 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"
)
const apiKey = "sk_live_xxxxx"
func main() {
resp, err := http.Get("http://localhost:3000/sse/crash?api_key=" + apiKey)
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