Server Sent Events — Experiment & Why We Need to Use it?

Sylvia Sari
6 min readOct 6, 2023

In this article, I will share about the experiment of Server Sent Events in my use case. I will explain why Server Sent Events become one of the alternatives that we can use.

Can you imagine that when the system is in a race condition between server’s processing data and client’s retrieving data? This is what happen in my use case. In this case, the processing data is via NSQ (asynchronous). Meanwhile, client retrieves data via API (synchronous).

Race condition happened between Process 2.1 and 2.2

From the above diagram:
1. Service A sends message that will be consumed by Service B.
2.1. At the same time or even faster than 2.2, Client will request to Service B to get the result from data that published by Service A.
2.2. Service B is still in the middle processing the message or even haven’t received the message yet.

The main problem here is client doesn’t know when the data is finished to process. It happened because of the race condition.

The Goals

Client no need to wait or request to Server manually. We can use one of alternatives that called Server Sent Events (SSE).

Disclaimer: This experiment is using REST API and based on personal experiment.

Server Sent Events (SSE) Overview

What is SSE?

Server-Sent Events (SSE) is a unidirectional (one way) data transmission to send events from server to client through HTTP. Client and server need to connect first. Once both of them already connect, server can send events until the connection is closed.

When to Use it?

  1. Progress updates. If we are in the middle of processing something and we want to know the progress, we can use SSE. For example, we are uploading file, we can show the percentage of the progress by using SSE.
  2. Realtime notification. We can broadcast news/information as soon as it’s available.
  3. Live data updates. For example, the position of other user (live location sharing) or drivers.

Pro & Cons

Pro and cons of SSE

WebSockets Overview

WebSockets is an advanced technology that allows real-time interactive bidirectional (two way) communication between a client and a server.

SSE vs WebSockets

Source: https://ably.com/blog/websockets-vs-sse

Why We use SSE compare to WebSockets?

For simple use case, we can implement SSE. This is a good choice. If we only need to push updates/information to user, SSE is enough. But for more complex use case, it’s better to use WebSockets. So, it depends on the use case. We can compare based on the pro and cons of SSE and WebSockets to decide which one we will use. For my use case, only Server that needs to send event. That’s why we chose SSE.

Experiment

The Flow

  1. Client opens connection to Server (Service B). Once connection is successfully created, Client will listen to the event that served by Service B.
  2. Service B sends message to Service B.
  3. Service B consumes the message and processes the data.
  4. Service B sends event that contained response to Client.
  5. Client handles the response, like showing something on the UI. Then, Client can close the connection.

Please do not close the connection if the system have retry mechanism. Let say, the system wanna do retry if processing data is failed. For this case, Server can send flag to retry or not.

Server side

Using a Rest API with Content-Type: text/event-stream .

w.Header().Set("Content-Type", "text/event-stream")

Client side

Using Javascript new EventSource with URL value is Server URL.

const esEvent = new EventSource(url);

Sample Code

Source code based on https://github.com/gurleensethi/go-server-sent-event-example.

Server Side

main.go

package main

import (
"fmt"
"net/http"
"strconv"
)

func main() {
http.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
return
}

w.Header().Set("Content-Type", "text/event-stream")
// Enable CORS because in this case, client and server are in different server/port.
enableCors(w)

isRetry, _ := strconv.ParseBool(r.FormValue("is_retry"))

// goroutine will generate the response
eventCh := make(chan EventResponse)
go generateResponse(r.Context(), eventCh, isRetry)

// Event name: test-event
for eventData := range eventCh {
event, err := buildResponse("test-event", eventData)
if err != nil {
break
}

_, err = fmt.Fprint(w, event)
if err != nil {
break
}

flusher.Flush()
}
})

http.ListenAndServe(":8888", nil)
}

func enableCors(w http.ResponseWriter) {
// Allow URL Client
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:8000")
// If Client use option WithCredentials: true, Server also need to allow Credentials.
// w.Header().Set("Access-Control-Allow-Credentials", "true")
}

In here, we use goroutine to have the retry mechanism:

event.go


package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"time"
)

type EventResponse struct {
Message string `json:"message"`
Timestamp string `json:"timestamp"`
IsRetry bool `json:"is_retry"`
}

func buildResponse(event string, data EventResponse) (string, error) {
m := map[string]EventResponse{
"data": data,
}

buff := bytes.NewBuffer([]byte{})

encoder := json.NewEncoder(buff)

err := encoder.Encode(m)
if err != nil {
return "", err
}

sb := strings.Builder{}

sb.WriteString(fmt.Sprintf("event: %s\n", event))
sb.WriteString(fmt.Sprintf("data: %v\n\n", buff.String()))

return sb.String(), nil
}

func generateResponse(ctx context.Context, eventCh chan<- EventResponse, isRetry bool) {
ticker := time.NewTicker(time.Second)

retryLoop:
for {
select {
case <-ctx.Done():
break retryLoop
case <-ticker.C:
time.Sleep(5 * time.Second)
if isRetry {
eventCh <- EventResponse{
Message: "Message are still in proccess. Please retry.",
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
IsRetry: true,
}
} else {
eventCh <- EventResponse{
Message: "You've received a message",
Timestamp: time.Now().Format("2006-01-02 15:04:05"),
}
}
}
}

ticker.Stop()

close(eventCh)
}

Client Side

main.go

package main

import (
_ "embed"
"net/http"
)

//go:embed index.html
var indexHTML []byte

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(indexHTML)
})
http.ListenAndServe(":8000", nil)
}

index.html

<html lang="en">
<head>
<title>Server Sent Events</title>
</head>

<body>
<strong>
<p id="title">Server Sent Events</p>
<p id="message">Loading Message...</p>
<p id="timestamp">Loading Time...
</strong>

<script>
// This is For UI
const messageEl = document.getElementById("message");
const timestampEl = document.getElementById("timestamp");

// URL Params
const urlParams = new URLSearchParams(window.location.search);
const isRetry = urlParams.get('is_retry');

var url = "http://localhost:8888/sse?is_retry=" + isRetry
const esEvent = new EventSource(url);
// We can it send option withCredentials. It will send cookie. The Server also need to allow Credentials.
// Sample Client side:
// const esEvent = new EventSource(url, {withCredentials : true});
// Only support withCredentials. not support other parameter like:
// const esEvent = new EventSource(url, {"user_id": userID});

// when there is error
esEvent.onerror = (err) => {
};

// when a connection successfully to open
esEvent.onopen = (...args) => {
};

// listen to event: test-event
esEvent.addEventListener("test-event", (event) => {
const resp = JSON.parse(event.data);
messageEl.innerText = resp.data.message;
timestampEl.innerText = resp.data.timestamp;

if (resp.data.is_retry){
setTimeout(() => {
messageEl.innerText = "Loading Message...";
timestampEl.innerText = "Loading Time...";
}, 3000);
}else{
// to close the connection. Once this is closed, the Client can not listen to the event anymore and need to reopen connection.
esEvent.close();
}
});
</script>
</body>
</html>

Result

As you can see on network, when Client requested to Server, the type is eventsource .

No Retry Mechanism

Once Client already got the response, Client will close the connection and won’t listen to the event anymore.

Client only listen once until Client got the response

With Retry Mechanism

Client will still listen to the event until Server response to stop retrying.

Client is retrying to request to Server
The response for is_retry : true. Client will retry again.

Based on the experiment, instead of Client requested manually, Client will listen to the event. Therefore, Client will be notified if there is updates/info. SSE is able to support this use case. For simple use case, like push updates, it’s good to give a try for SSE.

References

https://ably.com/topic/server-sent-events

https://ably.com/blog/websockets-vs-sse

https://ably.com/topic/what-are-websockets-used-for

https://javascript.info/server-sent-events

https://blog.logrocket.com/server-sent-events-vs-websockets/

https://github.com/gurleensethi/go-server-sent-event-example

Hope this article is useful for you!

--

--