Skip to main content

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
Latency10-50ms
ReconnectionAutomatic (browser native)
Browser SupportAll modern (except IE)

Endpoints

EndpointDescription
GET /sse/:gameStream by game slug
GET /sse/type/:typeStream by game type

Examples

http://localhost:3000/sse/crash
http://localhost:3000/sse/double
http://localhost:3000/sse/type/multiplier
https://datastream.hypetech.games/sse/crash

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) {
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

  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