Skip to content

Server-Sent Events (SSE)

Server-Sent Events (SSE) enables real-time streaming from HTTP ingresses to clients. When a client requests an SSE stream, the ingress keeps the connection open and pushes events as they occur.

Enabling SSE

SSE mode is automatically enabled when a client sends the Accept: text/event-stream header:

Terminal window
curl -N -H "Accept: text/event-stream" \
https://example.hantera.io/ingress/api/orders/ORD-123/events

The same HTTP ingress can serve both regular JSON responses and SSE streams based on the Accept header.

Message Format

SSE messages are sent as records with special fields that map to SSE protocol fields:

from {
event = 'orderUpdated'
data = { orderId = 'ORD-123', status = 'shipped' }
}

Output:

event: orderUpdated
data: {"orderId":"ORD-123","status":"shipped"}

Standard SSE Fields

FieldPurposeSerialization
eventEvent type for addEventListener()String
dataMain payloadJSON serialized
idEvent ID for reconnection trackingString
retryReconnection delay hint (ms)String

All other fields are serialized as strings and passed through to the client.

Example with All Fields

from {
event = 'heartbeat'
id = 'hb-42'
retry = 5000
data = { timestamp = now }
}

Output:

event: heartbeat
id: hb-42
retry: 5000
data: {"timestamp":"2026-01-11T19:30:00Z"}

Simple Values

Non-record values are automatically wrapped in data::

from 'Hello world'

Output:

data: "Hello world"

Real-Time Events with events()

The events() function subscribes to Hantera’s event bus, enabling real-time updates:

param orderId: text
from events ($'actors/order/{orderId}', 'checkpoint')
select e => { event = 'updated', data = e }

This subscribes to checkpoint events for a specific order actor and streams them to the client.

Complete Example: SKU Stock Updates

Here’s a real-world example streaming real-time stock availability for a SKU:

Ingress Configuration

uri: /resources/ingresses/api/skus/stock-events
spec:
type: http
componentId: sku-stock-events.hreactor
acl:
- skus:read
properties:
route: api/skus/{skuNumber}/stock
httpMethod: get

Component Code

import 'text'
param skuNumber: text
let safeSkuNumber = skuNumber replace("''", "''''")
let skuQuery = query skus(skuId, skuNumber)
filter $'skuNumber == ''{safeSkuNumber}'''
from skuQuery match
(e: QueryError) |> { error = { code = 'QUERY_ERROR', message = e.message } }
|>
let sku = skuQuery first
from sku match
nothing |> { error = { code = 'NOT_FOUND', message = 'SKU not found' } }
|>
let initialStock = messageActor(
'sku'
sku.skuId
[{ type = 'calculateAvailableStock' }]
)
from { event = 'init', data = initialStock }
from events ($'actors/sku/{sku.skuId}', 'checkpoint')
select e =>
let stock = messageActor(
'sku'
sku.skuId
[{ type = 'calculateAvailableStock' }]
)
from { event = 'stockUpdated', data = stock }

This ingress:

  1. Looks up the SKU by its skuNumber
  2. Sends an init event with the current available stock
  3. Streams stockUpdated events whenever the SKU’s stock changes

Client-Side Consumption

const skuNumber = 'PROD-001'
const eventSource = new EventSource(
`https://api.example.com/ingress/api/skus/${skuNumber}/stock`
)
eventSource.addEventListener('init', (e) => {
const stock = JSON.parse(e.data)
console.log('Initial stock:', stock)
updateStockDisplay(stock)
})
eventSource.addEventListener('stockUpdated', (e) => {
const stock = JSON.parse(e.data)
console.log('Stock updated:', stock)
updateStockDisplay(stock)
})
eventSource.onerror = (e) => {
console.log('Connection error, auto-reconnecting...')
}
// Clean up when done
function cleanup() {
eventSource.close()
}

Multiple Streams

You can combine multiple event sources and immediate values:

param orderId: uuid
param paymentId: uuid
from { event = 'init', data = { orderId, paymentId } }
from events ($'actors/order/{orderId}', 'checkpoint')
select e => { event = 'orderUpdated', data = e }
from events ($'actors/payment/{paymentId}', 'checkpoint')
select e => { event = 'paymentUpdated', data = e }

Multiple iterators are merged and events are delivered as they arrive from any source.

Error Handling

Pre-Stream Errors

Errors returned before streaming starts result in standard HTTP errors:

import 'text'
param skuNumber: text
let safeSkuNumber = skuNumber replace("''", "''''")
let skuQuery = query skus(skuId) filter $'skuNumber == ''{safeSkuNumber}'''
from skuQuery match
(e: QueryError) |> { error = { code = 'QUERY_ERROR', message = e.message } }
|>
let sku = skuQuery first
from sku match
nothing |> { error = { code = 'NOT_FOUND', message = 'SKU not found' } }
|> { event = 'init', data = sku }

If the SKU doesn’t exist, the client receives:

HTTP/1.1 404 Not Found
Content-Type: application/json
{"error":{"code":"NOT_FOUND","message":"SKU not found"}}

The EventSource.onerror handler will fire for HTTP error responses.

Application-Level Errors

For errors during streaming, send an error event:

from events (topic, eventType)
select e => e match
{ error: text } |> { event = 'error', data = { code = 'STREAM_ERROR', message = e.error } }
|> { event = 'updated', data = e }

Client handling:

eventSource.addEventListener('error', (e) => {
const error = JSON.parse(e.data)
console.error('Application error:', error)
})

Connection Errors

Connection drops trigger the onerror handler and automatic reconnection:

eventSource.onerror = (e) => {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('Reconnecting...')
} else if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connection closed')
}
}

Keepalive

Hantera sends keepalive comments every 30 seconds to maintain the connection:

:keepalive

These are automatically ignored by EventSource clients but prevent proxy timeouts.

Reconnection Behavior

The browser’s EventSource API automatically reconnects on connection loss:

  1. Connection drops
  2. Browser waits (default 3000ms, or value from retry field)
  3. Browser reconnects with Last-Event-ID header if IDs were sent

On reconnection, the component is re-executed. Design your component to handle this by always sending initial state:

from { event = 'init', data = currentState }
from events (topic, eventType)
select e => { event = 'updated', data = e }

Testing with cURL

Terminal window
curl -N -H "Accept: text/event-stream" \
https://example.hantera.io/ingress/api/skus/PROD-001/stock

The -N flag disables buffering for immediate output.

See Also