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:
- Native stream support - Ballerina's stream type maps perfectly to SSE
- Type safety - Compile-time checking for SSE events
- Simple syntax - Minimal boilerplate code
- Built-in error handling - Graceful error management
- 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};
    }
}
That's it! Save this as simple_sse.bal and run:
bal run simple_sse.bal
Testing with curl
curl -N http://localhost:8080/simple/events
Output:
event: counter
id: 1
data: Count: 1
event: counter
id: 2
data: Count: 2
event: counter
id: 3
data: Count: 3
...
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>
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)
|};
Example:
http:SseEvent event = {
    data: "Hello from server!",
    event: "greeting",
    id: "1",
    retry: 3000  // Client will retry after 3 seconds
};
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};
    }
}
Key Points:
- 
*object:Iterablemakes your class iterable
- 
iterator()returnsself, allowing the class to iterate over itself
- 
next()generates each event; returnnilto end the stream
- The isolatedqualifier 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;
}
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";
}
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>
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
}
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"
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"
};
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()};
    }
}
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};
}
6. Use Event IDs for Resumption
http:SseEvent event = {
    data: "...",
    id: self.eventCount.toString()  // Client can resume from this ID
};
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);
});
7. Set Appropriate Retry Times
http:SseEvent event = {
    data: "...",
    retry: 5000  // Client waits 5 seconds before reconnecting
};
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"}};
}
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
    }
}
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...
}
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:
- Ballerina HTTP Docs: https://lib.ballerina.io/ballerina/http/latest
- SSE Specification: https://html.spec.whatwg.org/multipage/server-sent-events.html
- MDN SSE Guide: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
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)