DEV Community

Cover image for Real-Time Data Streaming Made Simple: Server-Sent Events in Ballerina
Abhi
Abhi

Posted on

Real-Time Data Streaming Made Simple: Server-Sent Events in Ballerina

Introduction

In today's world of real-time applications—from live sports scores to stock tickers, chat notifications to IoT dashboards—the ability to push data from server to client is no longer a luxury, it's a necessity. While WebSockets often steal the spotlight, there's an elegant, simpler alternative that's perfect for many use cases: Server-Sent Events (SSE).

In this post, we'll explore how Ballerina, with its cloud-native design and powerful HTTP module, makes implementing SSE incredibly straightforward. We'll build real working examples, understand the patterns, and see why SSE might be exactly what your next project needs.

What Are Server-Sent Events?

Server-Sent Events (SSE) is a web technology that enables servers to push data to web clients over a single, long-lived HTTP connection. Think of it as a one-way communication channel from your server to the browser—perfect for scenarios where the server needs to continuously update the client.

The SSE Advantage

Why choose SSE over WebSockets?

Simpler to implement - Built on standard HTTP, no special protocols

Automatic reconnection - Browsers handle reconnection automatically

Event IDs & resume - Can resume from the last received event

Firewall friendly - Works over standard HTTP/HTTPS ports

Perfect for one-way updates - Server → Client communication

When to use SSE:

  • Real-time dashboards and monitoring
  • Live sports scores or stock tickers
  • News feeds and notifications
  • Progress indicators for long-running tasks
  • Social media updates
  • Server logs streaming

When NOT to use SSE:

  • Need bidirectional communication (use WebSockets)
  • Require binary data streaming
  • Need sub-millisecond latency

Why Ballerina for SSE?

Ballerina is a cloud-native programming language designed for network interactions and integration. Its HTTP module provides first-class support for SSE with a clean, type-safe API. Here's what makes Ballerina special for SSE:

  1. Native stream support - Ballerina's stream type maps perfectly to SSE
  2. Type safety - Compile-time checking for SSE events
  3. Simple syntax - Minimal boilerplate code
  4. Built-in error handling - Graceful error management
  5. Production-ready - Built for enterprise applications

Let's see it in action!

Your First SSE Application in 5 Minutes

Let's build a simple event counter that streams events to clients. This example demonstrates the core SSE pattern in Ballerina.

The Server Code

Here's a complete working SSE server in Ballerina:

import ballerina/http;
import ballerina/lang.runtime;

// Create an HTTP listener on port 8080
listener http:Listener simpleListener = new(8080);

// SSE service
service /simple on simpleListener {

    // Resource that returns a stream of SSE events
    resource function get events() returns stream<http:SseEvent, error?>|error {
        CounterStream counterStream = new CounterStream();
        stream<http:SseEvent, error?> eventStream = new(counterStream);
        return eventStream;
    }
}

// Stream implementation
class CounterStream {
    *object:Iterable;
    private int count = 0;
    private final int maxCount = 10;

    // Iterator method - required by Iterable
    public isolated function iterator() returns object {
        public isolated function next() returns record {|http:SseEvent value;|}|error?;
    } {
        return self;
    }

    // Next method - generates each event
    public isolated function next() returns record {|http:SseEvent value;|}|error? {

        // Stop after 10 events
        if self.count >= self.maxCount {
            return (); // Returning nil ends the stream
        }

        self.count += 1;

        // Create an SSE event
        http:SseEvent event = {
            data: string `Count: ${self.count}`,
            event: "counter",
            id: self.count.toString()
        };

        // Wait 1 second between events
        runtime:sleep(1);

        return {value: event};
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it! Save this as simple_sse.bal and run:

bal run simple_sse.bal
Enter fullscreen mode Exit fullscreen mode

Testing with curl

curl -N http://localhost:8080/simple/events
Enter fullscreen mode Exit fullscreen mode

Output:

event: counter
id: 1
data: Count: 1

event: counter
id: 2
data: Count: 2

event: counter
id: 3
data: Count: 3
...
Enter fullscreen mode Exit fullscreen mode

The Client Code

Here's a simple HTML client to visualize the events:

<!DOCTYPE html>
<html>
<head>
    <title>SSE Counter Demo</title>
    <style>
        body { font-family: Arial, sans-serif; padding: 20px; }
        #events { background: #f5f5f5; padding: 20px; border-radius: 5px; }
        .event { padding: 10px; margin: 5px 0; background: white; 
                 border-left: 4px solid #2196F3; }
    </style>
</head>
<body>
    <h1>SSE Counter Demo</h1>
    <div id="status">Disconnected</div>
    <div id="events"></div>

    <script>
        const eventSource = new EventSource('http://localhost:8080/simple/events');
        const eventsDiv = document.getElementById('events');
        const statusDiv = document.getElementById('status');

        eventSource.addEventListener('open', () => {
            statusDiv.textContent = 'Connected ✅';
            statusDiv.style.color = 'green';
        });

        eventSource.addEventListener('counter', (event) => {
            const eventDiv = document.createElement('div');
            eventDiv.className = 'event';
            eventDiv.innerHTML = `
                <strong>Event ID:</strong> ${event.lastEventId}<br>
                <strong>Data:</strong> ${event.data}<br>
                <strong>Time:</strong> ${new Date().toLocaleTimeString()}
            `;
            eventsDiv.insertBefore(eventDiv, eventsDiv.firstChild);
        });

        eventSource.addEventListener('error', () => {
            statusDiv.textContent = 'Disconnected ❌';
            statusDiv.style.color = 'red';
        });
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Open this HTML file in your browser, and watch the events stream in real-time!

Understanding the SSE Pattern in Ballerina

Let's break down the key components:

1. The http:SseEvent Record

Every SSE event in Ballerina is represented by the http:SseEvent record:

type SseEvent record {|
    string data;       // The event payload (REQUIRED)
    string event?;     // Event type name (optional)
    string id?;        // Event identifier (optional)
    int retry?;        // Reconnection time in ms (optional)
    string comment?;   // Comment line (optional)
|};
Enter fullscreen mode Exit fullscreen mode

Example:

http:SseEvent event = {
    data: "Hello from server!",
    event: "greeting",
    id: "1",
    retry: 3000  // Client will retry after 3 seconds
};
Enter fullscreen mode Exit fullscreen mode

2. The Stream Class Pattern

Every SSE stream in Ballerina follows this pattern:

class MyEventStream {
    *object:Iterable;  // Step 1: Implement Iterable

    // Step 2: Implement iterator() method
    public isolated function iterator() returns object {
        public isolated function next() returns record {|http:SseEvent value;|}|error?;
    } {
        return self;
    }

    // Step 3: Implement next() method
    public isolated function next() returns record {|http:SseEvent value;|}|error? {
        // Return nil to end the stream
        if shouldEnd {
            return ();
        }

        // Create and return event
        http:SseEvent event = { /* ... */ };
        return {value: event};
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Points:

  • *object:Iterable makes your class iterable
  • iterator() returns self, allowing the class to iterate over itself
  • next() generates each event; return nil to end the stream
  • The isolated qualifier ensures thread safety

3. The Resource Function

resource function get events() returns stream<http:SseEvent, error?>|error {
    MyEventStream myStream = new MyEventStream();
    stream<http:SseEvent, error?> eventStream = new(myStream);
    return eventStream;
}
Enter fullscreen mode Exit fullscreen mode

The resource function creates the stream and returns it. Ballerina handles all the SSE protocol details automatically!

Real-World Example: Live Stock Ticker

Let's build something more practical—a real-time stock price ticker. This example demonstrates:

  • Dynamic data generation
  • Multiple symbols
  • URL path parameters
  • Realistic timing

The Stock Ticker Server

import ballerina/http;
import ballerina/lang.runtime;
import ballerina/random;

type Stock record {|
    string symbol;
    decimal price;
    decimal change;
    string timestamp;
|};

listener http:Listener stockListener = new(9090);

service /stocks on stockListener {

    // Stream stock updates for specified symbols
    // Example: /stocks/stream/AAPL,GOOGL,MSFT
    resource function get stream/[string symbols]() returns stream<http:SseEvent, error?>|error {
        string[] symbolList = re `,`.split(symbols);
        StockPriceStream stockStream = new StockPriceStream(symbolList);
        return new(stockStream);
    }
}

class StockPriceStream {
    *object:Iterable;
    private string[] symbols;
    private int eventCount = 0;
    private final int maxEvents = 50;

    function init(string[] symbols) {
        self.symbols = symbols;
    }

    public isolated function iterator() returns object {
        public isolated function next() returns record {|http:SseEvent value;|}|error?;
    } {
        return self;
    }

    public isolated function next() returns record {|http:SseEvent value;|}|error? {
        if self.eventCount >= self.maxEvents {
            return ();
        }

        // Select random stock
        int randomIndex = check random:createIntInRange(0, self.symbols.length());
        string symbol = self.symbols[randomIndex];

        // Generate realistic price data
        decimal basePrice = <decimal>(50.0 + <float>(check random:createIntInRange(0, 950)));
        decimal change = <decimal>(<float>(check random:createIntInRange(-500, 500)) / 100.0);
        decimal currentPrice = basePrice + change;

        Stock stock = {
            symbol: symbol.toUpperAscii(),
            price: currentPrice,
            change: change,
            timestamp: getCurrentTimestamp()
        };

        http:SseEvent event = {
            data: stock.toJsonString(),
            event: "stock-update",
            id: self.eventCount.toString()
        };

        self.eventCount += 1;
        runtime:sleep(1);

        return {value: event};
    }
}

function getCurrentTimestamp() returns string {
    // Simplified - in production, use time:utcNow()
    return "2025-10-30T19:00:00Z";
}
Enter fullscreen mode Exit fullscreen mode

The Stock Ticker Client

<!DOCTYPE html>
<html>
<head>
    <title>Live Stock Ticker</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
            padding: 20px;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: white;
            border-radius: 10px;
            padding: 30px;
        }
        .stock-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
            margin-top: 20px;
        }
        .stock-card {
            padding: 20px;
            border: 2px solid #e0e0e0;
            border-radius: 10px;
            transition: all 0.3s;
        }
        .stock-card.updated {
            animation: pulse 0.5s;
            border-color: #2196F3;
        }
        @keyframes pulse {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.05); }
        }
        .symbol {
            font-size: 24px;
            font-weight: bold;
            color: #333;
        }
        .price {
            font-size: 36px;
            font-weight: bold;
            margin: 10px 0;
        }
        .change {
            padding: 5px 10px;
            border-radius: 5px;
            font-weight: bold;
        }
        .positive {
            background: #d4edda;
            color: #155724;
        }
        .negative {
            background: #f8d7da;
            color: #721c24;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📈 Live Stock Ticker</h1>
        <div>
            <input type="text" id="symbols" placeholder="AAPL,GOOGL,MSFT,TSLA" 
                   value="AAPL,GOOGL,MSFT,TSLA">
            <button onclick="connect()">Connect</button>
            <button onclick="disconnect()">Disconnect</button>
        </div>
        <div id="status">Disconnected</div>
        <div class="stock-grid" id="stockGrid"></div>
    </div>

    <script>
        let eventSource = null;
        const stockData = new Map();

        function connect() {
            const symbols = document.getElementById('symbols').value;
            if (eventSource) disconnect();

            eventSource = new EventSource(`http://localhost:9090/stocks/stream/${symbols}`);

            eventSource.addEventListener('open', () => {
                document.getElementById('status').textContent = '✅ Connected';
                document.getElementById('status').style.color = 'green';
            });

            eventSource.addEventListener('stock-update', (event) => {
                const stock = JSON.parse(event.data);
                updateStock(stock);
            });

            eventSource.addEventListener('error', () => {
                document.getElementById('status').textContent = '❌ Disconnected';
                document.getElementById('status').style.color = 'red';
            });
        }

        function disconnect() {
            if (eventSource) {
                eventSource.close();
                eventSource = null;
            }
        }

        function updateStock(stock) {
            stockData.set(stock.symbol, stock);
            renderStocks();
        }

        function renderStocks() {
            const grid = document.getElementById('stockGrid');
            grid.innerHTML = '';

            stockData.forEach((stock, symbol) => {
                const card = document.createElement('div');
                card.className = 'stock-card updated';

                const changeClass = stock.change >= 0 ? 'positive' : 'negative';
                const changeSign = stock.change >= 0 ? '+' : '';

                card.innerHTML = `
                    <div class="symbol">${stock.symbol}</div>
                    <div class="price">$${parseFloat(stock.price).toFixed(2)}</div>
                    <div class="change ${changeClass}">
                        ${changeSign}${parseFloat(stock.change).toFixed(2)}
                    </div>
                `;

                grid.appendChild(card);

                setTimeout(() => card.classList.remove('updated'), 500);
            });
        }
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Best Practices for SSE in Ballerina

1. Always Handle Stream Termination

public isolated function next() returns record {|http:SseEvent value;|}|error? {
    // Always provide a way to end the stream
    if self.count >= self.maxCount {
        return (); // End stream gracefully
    }
    // ... generate event
}
Enter fullscreen mode Exit fullscreen mode

2. Use Meaningful Event Types

// Good - descriptive event types
event: "stock-update"
event: "news-article"
event: "user-notification"

// Bad - generic names
event: "data"
event: "msg"
Enter fullscreen mode Exit fullscreen mode

3. Always Send JSON for Complex Data

// Convert records to JSON strings
type StockData record {| string symbol; decimal price; |};
StockData stock = {symbol: "AAPL", price: 150.00};

http:SseEvent event = {
    data: stock.toJsonString(),  // ← Use toJsonString()
    event: "stock-update"
};
Enter fullscreen mode Exit fullscreen mode

4. Implement Proper Error Handling

public isolated function next() returns record {|http:SseEvent value;|}|error? {
    // Use check for operations that can fail
    int randomValue = check random:createIntInRange(0, 100);

    // Or use try-catch for more control
    do {
        int value = check riskyOperation();
        return {value: createEvent(value)};
    } on fail error e {
        // Log error and return safe event
        log:printError("Error generating event", e);
        return {value: createErrorEvent()};
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Control Event Frequency

import ballerina/lang.runtime;

public isolated function next() returns record {|http:SseEvent value;|}|error? {
    // Add delay between events to prevent overwhelming clients
    runtime:sleep(1);  // Wait 1 second

    // Generate and return event
    return {value: event};
}
Enter fullscreen mode Exit fullscreen mode

6. Use Event IDs for Resumption

http:SseEvent event = {
    data: "...",
    id: self.eventCount.toString()  // Client can resume from this ID
};
Enter fullscreen mode Exit fullscreen mode

Clients can use the Last-Event-ID header to resume from where they left off:

const lastEventId = localStorage.getItem('lastEventId');
const eventSource = new EventSource('/events', {
    headers: { 'Last-Event-ID': lastEventId }
});

eventSource.addEventListener('message', (event) => {
    localStorage.setItem('lastEventId', event.lastEventId);
});
Enter fullscreen mode Exit fullscreen mode

7. Set Appropriate Retry Times

http:SseEvent event = {
    data: "...",
    retry: 5000  // Client waits 5 seconds before reconnecting
};
Enter fullscreen mode Exit fullscreen mode

8. Provide Stream Status Events

// Send periodic heartbeat
if self.eventCount % 10 == 0 {
    return {value: {event: "heartbeat", data: "alive"}};
}

// Send stream completion event
if self.count == self.maxCount {
    return {value: {event: "stream-end", data: "completed"}};
}
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Memory Management

class EfficientStream {
    *object:Iterable;
    private final int maxEvents = 100;  // Limit total events

    public isolated function next() returns record {|http:SseEvent value;|}|error? {
        // Clean up resources periodically
        if self.eventCount >= self.maxEvents {
            self.cleanup();
            return ();
        }
        // ... generate event
    }

    function cleanup() {
        // Release resources
    }
}
Enter fullscreen mode Exit fullscreen mode

Connection Limits

Monitor active SSE connections and implement limits:

// In a real application, track active connections
isolated int activeConnections = 0;
final int MAX_CONNECTIONS = 1000;

resource function get events() returns stream<http:SseEvent, error?>|error {
    lock {
        if activeConnections >= MAX_CONNECTIONS {
            return error("Too many active connections");
        }
        activeConnections += 1;
    }

    // Return stream...
}
Enter fullscreen mode Exit fullscreen mode

Comparison: SSE vs WebSockets vs HTTP Polling

Feature SSE WebSockets HTTP Polling
Connection One-way (Server → Client) Two-way Request-Response
Protocol HTTP WebSocket (ws://) HTTP
Complexity Low Medium Low
Auto-Reconnect Yes (built-in) Manual N/A
Event IDs Yes Manual No
Browser Support Excellent Excellent Universal
Firewall Friendly Yes Sometimes blocked Yes
Use Case Real-time updates Chat, gaming Simple updates
Efficiency High Highest Low

Resources:

Happy Streaming! 🚀

If you found this helpful, share it with your team and let us know what you build with SSE in Ballerina!

Questions or feedback? Drop a comment below or reach out to the Ballerina community.

Top comments (0)