The Server Side of Server-Sent Events

Server-sent events are an underutilized API for implementing push technology.  Server-sent events transmit data to clients as a continuous stream, referred to as an event stream, over a connection which is left open.  By maintaining an open connection, the overhead of repeatedly establishing a new connection is eliminated.

Unfortunately, many developers are completely unaware that server-sent events exist because they are often overshadowed by the more powerful WebSockets API.  However, server-sent events do have certain advantages over WebSockets.  For example, server-sent events support custom message types and automatic reconnection for dropped connections.  These features can be implemented in WebSockets, but they are available by default with server-sent events.  WebSockets applications also require servers that support the WebSockets protocol.  By comparison, server-sent events are built atop HTTP and can be implemented in standard web servers.

This post covers the server side of server-sent events.  I will be publishing a related post covering the client side soon.  If you are just looking for some example code to get up and running, check out my post, “Server-Sent Events in Node.js“.

Establishing an Event Stream

Event streams are plain text responses served over a connection which is kept alive.  To establish an event stream, the server must respond to a client request with a HTTP 200 OK response.  Event streams are served with the “text/event-stream” MIME type, and should not be cached.  To setup an event stream, the server should provide the following HTTP headers.

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Stream Syntax

Event streams are a series of lines of text.  Each line is comprised of a field name, a colon, the field’s value, and a newline character (“\n”).  An individual event is a group of one or more lines, separated by an additional newline character.  The following example shows the generic format of a single line in an event stream.

field: value\n

The field name must be either “data”, “event”, “id”, or “retry”.  If the field name is not one of these values, then the line is ignored.  The meaning of each field name will be explained later.  A line beginning with a colon is treated as a comment.  Comments are often transmitted periodically to keep a connection alive if events are sent infrequently.  The following example shows an event stream comment.

: This is a comment\n

Sending Events

A “data” line is interpreted as the payload of an event.  The following example shows an event in its most basic form.  Notice that an additional newline character has been appended to the line to denote the end of the event.

data:  message\n\n

Events can also be spread across multiple lines to aid readability.  The following example shows a multi-line event.  Notice that the first line ends with a single newline character, while the second line ends with two.

data: begin message\n
data: continue message\n\n

Multi-line events are particularly useful for working with JSON strings.  The following example inserts a JSON string into the event stream.  On the client side, the JSON string can later be processed using the JSON.parse() method.

data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

Assigning Event Identifiers

The “id” field can be used to assign identifiers to individual events.  The following example creates an event with the ID “msg1″.

id: msg1\n
data: message\n\n

On the client side, the browser keeps track of the most recently received event ID.  If the connection fails, the client will attempt to reconnect, and send a special “Last-Event-ID” HTTP header containing the event ID.  This header can be used as a synchronization mechanism between the client and server.  The header can also be reset by setting the ID to the empty string.

Named Events

A single stream can generate multiple types of events by assigning names to different classes of events.  For example, a sports ticker might generate events for scoring plays, injury updates, and various other game information.  Distinguishing between event types allows the client to handle them separately.  The “event” field is used to assign a name to an individual event.  The following example stream creates three types of events.  The first event is a “foo” named event.  The second event does not specify an “event” field, and is therefore an unnamed event.  The final event is another named event of type “bar”.  Note that the “event” line follows the “data” line in the last event.  To compensate for the ordering, the “data” line ends in a single newline character, while the “event” line ends with two newlines.

event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
data: a bar event\n
event: bar\n\n

Specifying Reconnect Time

If an event stream’s connection is dropped, the client will automatically attempt to reconnect after a certain amount of time ― typically around three seconds.  The server can define the reconnection time by sending a “retry” line.  The reconnection time is specified in milliseconds as a base ten integer.  The following example sets the stream’s reconnection time to ten seconds.

retry: 10000\n

Choosing the Right Server Software

Because server-sent events operate over HTTP, they can be used with standard web servers.  However, because server-sent events maintain an open connection, they are not particularly well suited to some web servers.  Apache, for example, delegates an operating system thread to each request.  On a busy machine, server-sent events can quickly consume the pool of available threads.

Unlike Apache, Node.js is entirely single-threaded.  When Node.js receives a request, it is handled by a function call, rather than a thread.  This model allows Node.js to more easily scale to large number of open connections.  The caveat is that none of the connections can be allowed to perform high latency blocking operations.  Because all of the connections are processed by a single thread, any one connection has the potential to stall the entire system.  However, if adequate response times can be guaranteed, then event driven servers like Node.js are better choices for implementing server-sent events.

Things to Remember

  • Server-sent events implement push technology over an HTTP connection which is kept alive.
  • Event streams are served with the “text/event-stream” MIME type, and should not be cached.
  • Event data is specified using “data” lines.
  • “id” lines are used to assign identifiers to individual events.
  • A single event stream can contain multiple types of events by using “event” lines.
  • Disrupted event streams automatically attempt to reconnect after approximately three seconds.  This time can be adjusted by using a “retry” line.

5 thoughts on “The Server Side of Server-Sent Events

  1. I think your mention of using “retry” are not entirely correct. I don’t see in the specs that it’s just for disrupted connections. Try writing a spring mvc app, or webservlet that returns system time every one second… how do you do this if default time is 3 seconds? You use “retry:1000\n” … but doesn’t mean connection has been disrupted per se, it means don’t use default interval. Chrome and Safari handle this fine, Firefox has issues. Without retry (set around 3 seconds default) the event would only happen once.

    • According to the spec, the “retry” field is used to set the event stream’s reconnection time. The reconnection time is then used in the algorithm for reestablishing the connection. Unfortunately, server-sent events can be tricky in some servers like Apache. Node.js handles them well though.

      • Yes, that’s right — even gracefully dropped connections, network hiccups etc, and 200OK response. It looks like you reworded above. I have no problems with Spring MVC controller app or webservlet app through Tomcat 7 using retry interval, except with Firefox. It doesn’t seem to recognize the retry for more than 10-20 minutes with 200OK responses (then ignores the retry.) Chrome and Safari work fine and reconnect forever. I opened a bug with Mozilla on this. In the past I’ve used DWR and Atmosphere to persist a connection for server side events/requests. They seem to be more widely supported with better fallback techniques than imsplementing your own polyfill.