Pular para o conteúdo principal

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

PropertyValue
ProtocolHTTP (chunked transfer)
DirectionServer → Client
Latency~50ms
ReconnectionAutomatic (browser native)
Browser SupportAll modern (except IE)

Data Flow

SSE receives data through the same pipeline as WebSocket, but with HTTP chunked transfer:

SSE Data Flow

StepComponentActionLatency
1PostgresINSERT INTO rounds-
2DebeziumReads WAL, publishes to NATS~1-5ms
3NATSDelivers to Consumer<1ms
4ConsumerProcesses, publishes to Redis<1ms
5Redis Pub/SubDelivers to Backend<1ms
6BackendHTTP chunked response~10-50ms
7BrowserEventSource API-

Total end-to-end latency: ~50ms

Why higher latency?

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)}`);
dica

See the Authentication Guide for detailed information on creating and managing API keys.

Endpoints

EndpointDescription
GET /sse/:game?api_key=xxxStream by game slug
GET /sse/type/:type?api_key=xxxStream 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

EventDescription
initialFirst message with latest result
messageNew 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

  1. Let the browser handle reconnection - SSE auto-reconnects on disconnect
  2. Use Last-Event-ID - For resuming from last received event
  3. Implement heartbeats - Keep connection alive through proxies

Message Handling

  1. Use named events - Differentiate message types
  2. Keep payloads small - SSE is text-only
  3. Parse errors gracefully - Handle malformed JSON

Performance

  1. Use HTTP/2 - Multiple streams over single connection
  2. Enable compression - gzip for text data
  3. Set appropriate timeouts - Prevent zombie connections

Advantages of SSE

AdvantageDescription
SimplicityJust HTTP, no special protocol
Auto-reconnectBrowser handles reconnection
Event IDResume from last event
Proxy-friendlyWorks through HTTP proxies
Text-basedEasy to debug with curl

Limitations

LimitationWorkaround
Server → Client onlyUse REST API for client → server
Text onlyBase64 encode binary data
6 connections per domainUse HTTP/2 or single multiplexed stream
No IE supportUse 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:

  1. Check Redis Pub/Sub is working
  2. Verify consumer is running
  3. Check game slug is valid
  4. 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

FeatureSSEWebSocket
DirectionServer → ClientBidirectional
ProtocolHTTPWebSocket
Auto-reconnectYesNo
Binary dataNoYes
Browser supportIE exceptUniversal
Proxy-friendlyYesSometimes
Debug with curlYesNo

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