Skip to main content

Using SSE

This document describes how to implement SSE (Server-Sent Events) server push in HTTP Cloud Functions to support unidirectional real-time data streaming from server to clients.

What is SSE

「SSE (Server-Sent Events)」is a technology for servers to push real-time data to clients, based on the HTTP protocol. Unlike WebSocket, SSE is unidirectional communication - it only supports server-to-client data push, while clients send data through regular HTTP requests.

Key Features:

  • Unidirectional Push: Server actively pushes data to clients; clients can only receive
  • HTTP-based: Uses standard HTTP protocol with good compatibility and easy implementation
  • Auto Reconnection: Clients automatically reconnect after disconnection
  • Text Format: Transmitted data is in text format (usually JSON)
  • Lightweight: Simpler implementation and lower resource usage compared to WebSocket

Typical Use Cases:

  • AI conversation streaming output
  • Real-time log push
  • Progress update notifications
  • Server status monitoring
  • Real-time data dashboards

Basic Usage

1. Creating SSE Cloud Function

Get SSE instance through context.sse() in Cloud Functions, then use the send() method to push data.

exports.main = async function (event, context) {
// Get SSE instance
const sse = context.sse?.();

if (sse && !sse.closed) {
// Send message
sse.send({
data: "Hello from SSE!"
});

// End connection
sse.end({
data: "Goodbye!"
});
}

return "";
};

2. Sending Multiple Messages

exports.main = async function (event, context) {
const sse = context.sse?.();

if (sse && !sse.closed) {
// Send first message
sse.send({
data: "First message"
});

// Send second message
sse.send({
data: "Second message"
});

// Send JSON data
sse.send({
data: { message: "This is JSON data", timestamp: Date.now() }
});

// End connection
sse.end({
data: "All messages sent"
});
}

return "";
};

3. Streaming Data Push

Simulate streaming scenarios like AI conversations:

exports.main = async function (event, context) {
const sse = context.sse?.();

if (sse && !sse.closed) {
const text = "This is a streaming text output, each character will be pushed individually to the client.";

// Push character by character
for (let i = 0; i < text.length; i++) {
if (sse.closed) break; // Check if connection is closed

sse.send({
data: text[i]
});

// Simulate delay
await new Promise(resolve => setTimeout(resolve, 100));
}

// End connection
sse.end({
data: "[Complete]"
});
}

return "";
};

SSE API

context.sse() Method

Calling context.sse() gets an SSE instance. Supports passing configuration parameters:

const sse = context.sse?.({
keepalive: false, // Keep connection alive, default true
headers: {
'X-Custom-Header': 'value',
'X-Session-ID': 'session-123',
}
});

Configuration Parameters

ParameterTypeDefaultDescription
keepalivebooleantrueKeep connection alive
headersRecord<string, string \| string[]>{}Custom response headers

ISeverSentEvent Interface

The SSE instance returned by context.sse() provides the following methods and properties:

Methods

interface ISeverSentEvent {
setEncoder(encoder: TextEncoder): void;
send(event: SseEvent | SseEvent[] | string | string[]): boolean;
end(msg?: SseEvent): void;
on(event: 'close', callback: () => void): void;
}
MethodDescriptionReturn Value
send(event)Send SSE eventboolean - Whether send succeeded
end(msg?)End SSE connection, optionally send last messagevoid
setEncoder(encoder)Set text encodervoid
on('close', callback)Listen to connection close eventvoid

Properties

PropertyTypeDescription
closedbooleanWhether connection is closed

SseEvent Data Structure

interface SseEvent<T = string | Record<string, unknown>> {
data: T; // Data to send
comment?: string; // Comment (not processed by client)
event?: string; // Event type
id?: string; // Event ID (for reconnection)
retry?: number; // Reconnection time (milliseconds)
}

Complete Examples

1. AI Conversation Streaming Output

exports.main = async function (event, context) {
const sse = context.sse?.();

if (!sse || sse.closed) {
return { error: "Cannot establish SSE connection" };
}

// Listen to connection close
sse.on("close", () => {
console.log("Client disconnected");
});

try {
// Parse user message
const { message } = event;

// Send start marker
sse.send({
event: "start",
data: { status: "Starting reply generation" }
});

// Simulate AI generating reply (can call AI model in practice)
const reply = "This is AI-generated reply content, pushed character by character to user.";

for (let i = 0; i < reply.length; i++) {
if (sse.closed) break;

sse.send({
event: "message",
data: { content: reply[i], index: i }
});

await new Promise(resolve => setTimeout(resolve, 50));
}

// Send completion marker
sse.end({
event: "done",
data: { status: "Generation complete", totalLength: reply.length }
});

} catch (error) {
// Send error info
sse.end({
event: "error",
data: { message: error.message }
});
}

return "";
};

2. Real-time Progress Push

exports.main = async function (event, context) {
const sse = context.sse?.();

if (!sse || sse.closed) {
return { error: "Cannot establish SSE connection" };
}

// Simulate long-running task
async function longRunningTask() {
const totalSteps = 10;

for (let step = 1; step <= totalSteps; step++) {
if (sse.closed) break;

// Send progress update
sse.send({
event: "progress",
data: {
step,
total: totalSteps,
percent: Math.round((step / totalSteps) * 100),
message: `Processing step ${step}...`
}
});

// Simulate task execution
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

await longRunningTask();

// Task complete
sse.end({
event: "complete",
data: { status: "Task complete", timestamp: Date.now() }
});

return "";
};

3. Real-time Log Push

// Log queue (can use message queue in practice)
const logQueue = [];

exports.main = async function (event, context) {
const sse = context.sse?.();

if (!sse || sse.closed) {
return { error: "Cannot establish SSE connection" };
}

// Listen to connection close
let intervalId;
sse.on("close", () => {
clearInterval(intervalId);
console.log("Stopped pushing logs");
});

// Push logs periodically
intervalId = setInterval(() => {
if (sse.closed) {
clearInterval(intervalId);
return;
}

// Get logs from queue
const logs = getLatestLogs(); // Actual implementation

if (logs.length > 0) {
// Batch send logs
sse.send(logs.map(log => ({
event: "log",
data: {
level: log.level,
message: log.message,
timestamp: log.timestamp
}
})));
}
}, 1000);

// Keep connection
return "";
};

// Simulate getting logs
function getLatestLogs() {
// In practice, can get from database or logging system
return [
{ level: "info", message: "Application started", timestamp: Date.now() },
{ level: "debug", message: "Processing request", timestamp: Date.now() }
];
}

4. Server Status Monitoring

exports.main = async function (event, context) {
const sse = context.sse?.({
headers: {
'X-Monitoring-Session': `session-${Date.now()}`
}
});

if (!sse || sse.closed) {
return { error: "Cannot establish SSE connection" };
}

// Monitoring data push
let intervalId = setInterval(() => {
if (sse.closed) {
clearInterval(intervalId);
return;
}

// Get system status
const status = {
cpu: Math.random() * 100,
memory: Math.random() * 100,
activeConnections: Math.floor(Math.random() * 1000),
timestamp: Date.now()
};

sse.send({
event: "metrics",
data: status
});
}, 2000);

// Set timeout (optional)
setTimeout(() => {
clearInterval(intervalId);
sse.end({
event: "timeout",
data: { message: "Monitoring session timeout" }
});
}, 300000); // 5 minutes timeout

return "";
};

Advanced Usage

1. Sending Raw SSE Messages

SSE protocol supports multiple fields (data, event, id, retry etc.), can send raw message strings:

exports.main = async function (event, context) {
const sse = context.sse?.();

if (sse && !sse.closed) {
// Send raw message (note: must end with newline)
sse.send("data: This is a raw message\n\n");

// Batch send raw messages
sse.send([
"event: custom\n",
"data: Custom event\n",
"id: 123\n",
"retry: 1000\n",
"\n"
]);

sse.end();
}

return "";
};

2. Batch Sending Multiple Events

exports.main = async function (event, context) {
const sse = context.sse?.();

if (sse && !sse.closed) {
// Send multiple events at once
sse.send([
{ event: "message", data: "First message" },
{ event: "message", data: "Second message" },
{ event: "message", data: "Third message" }
]);

sse.end();
}

return "";
};

3. Handling Line Breaks

Line breaks have special meaning in SSE protocol, pay attention when sending data with line breaks:

exports.main = async function (event, context) {
const sse = context.sse?.();

if (sse && !sse.closed) {
// Message with line breaks
sse.send({
data: "Line one\\nLine two\\nLine three"
});

// Multi-line text
const multilineText = `This is line one
This is line two
This is line three`;

sse.send({
data: multilineText
});

sse.end();
}

return "";
};

4. Custom Event Types

exports.main = async function (event, context) {
const sse = context.sse?.();

if (sse && !sse.closed) {
// Different event types
sse.send({
event: "notification",
data: { type: "info", message: "This is a notification" }
});

sse.send({
event: "alert",
data: { type: "warning", message: "This is a warning" }
});

sse.send({
event: "update",
data: { type: "success", message: "Update successful" }
});

sse.end();
}

return "";
};

Client Connection Examples

Browser JavaScript

// Create SSE connection
const eventSource = new EventSource('https://your-service.run.tcloudbase.com/sse');

// Listen to default message event
eventSource.onmessage = (event) => {
console.log('Received message:', event.data);

try {
const data = JSON.parse(event.data);
console.log('Parsed data:', data);
} catch (e) {
// Handle non-JSON data
}
};

// Listen to custom events
eventSource.addEventListener('notification', (event) => {
console.log('Received notification:', event.data);
});

eventSource.addEventListener('progress', (event) => {
const data = JSON.parse(event.data);
console.log(`Progress: ${data.percent}%`);
});

// Listen to connection open
eventSource.onopen = () => {
console.log('SSE connection established');
};

// Listen to errors
eventSource.onerror = (error) => {
console.error('SSE error:', error);

// Can choose to close connection
// eventSource.close();
};

// Close connection
function closeConnection() {
eventSource.close();
console.log('SSE connection closed');
}

React Example

import { useEffect, useState } from 'react';

function StreamingChat() {
const [messages, setMessages] = useState([]);
const [currentMessage, setCurrentMessage] = useState('');

useEffect(() => {
const eventSource = new EventSource('https://your-service.run.tcloudbase.com/sse');

eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
setCurrentMessage(prev => prev + data.content);
});

eventSource.addEventListener('done', () => {
setMessages(prev => [...prev, currentMessage]);
setCurrentMessage('');
eventSource.close();
});

eventSource.onerror = (error) => {
console.error('Connection error:', error);
eventSource.close();
};

return () => {
eventSource.close();
};
}, []);

return (
<div>
{messages.map((msg, index) => (
<div key={index}>{msg}</div>
))}
{currentMessage && <div>{currentMessage}</div>}
</div>
);
}

Node.js Client

const EventSource = require('eventsource');

const eventSource = new EventSource('https://your-service.run.tcloudbase.com/sse');

eventSource.onmessage = (event) => {
console.log('Received message:', event.data);
};

eventSource.addEventListener('progress', (event) => {
const data = JSON.parse(event.data);
console.log(`Progress: ${data.percent}%`);
});

eventSource.onerror = (error) => {
console.error('Error:', error);
};

Using TypeScript

import { TcbEventFunction } from "@cloudbase/functions-typings";

interface MessageData {
content: string;
timestamp: number;
}

export const main: TcbEventFunction<void, Promise<string>> = async function (
event,
context
) {
const sse = context.sse?.({
keepalive: true,
headers: {
'X-Session-ID': 'session-123'
}
});

if (!sse || sse.closed) {
return JSON.stringify({ error: "Cannot establish SSE connection" });
}

sse.on("close", () => {
console.log("SSE connection closed");
});

// Send message
sse.send({
event: "message",
data: {
content: "Hello from SSE!",
timestamp: Date.now()
} as MessageData
});

// End connection
sse.end({
event: "complete",
data: { status: "Complete" }
});

return "";
};

Important Notes

⚠️ Note: The following points require special attention when using SSE

  • Unidirectional Communication: SSE only supports server-to-client push; clients need other methods (like HTTP POST) to send data to server
  • Connection Persistence: SSE connections occupy resources long-term; need reasonable timeout mechanisms and connection limits
  • Browser Limits: Browsers have SSE connection limits per domain (usually 6); need connection management
  • Auto Reconnection: Clients automatically reconnect; servers need to handle duplicate connections
  • Text Format: SSE can only transmit text data; complex data needs JSON serialization
  • Error Handling: Properly handle connection close and error cases to avoid resource leaks
  • Return Value: Cloud Functions using SSE must return empty string or no content
  • Content-Type: SSE response Content-Type is text/event-stream, automatically set by framework

SSE vs WebSocket

FeatureSSEWebSocket
CommunicationUnidirectional (Server→Client)Bidirectional (Server↔Client)
ProtocolHTTPWebSocket protocol
Data FormatText (usually JSON)Text or binary
Auto ReconnectYesNo (manual implementation needed)
Browser CompatibilityGood (except IE)Good
Implementation ComplexitySimpleRelatively complex
Resource UsageLowerHigher
Use CasesUnidirectional data pushReal-time bidirectional communication

Selection Guide:

  • Only need server push: Choose SSE
  • Need bidirectional real-time communication: Choose WebSocket
  • Prefer simple implementation: Choose SSE
  • Need binary data transmission: Choose WebSocket