Notes

Serving Datastar Streams

Datastar leaves the server-side implementation open-ended. Datastar provides SDKs for various languages, but those SDKs only cover the protocol on top of SSE that the Datastar JavaScript client expects. To manage Datastar connections from the server, you need two things:

  1. Sending the SSE events Datastar expects.
  2. Getting state changes from one of your servers to the others.

The second is the harder one if you haven’t worked with streaming architectures. The creator of Datastar uses NATS, but any centralized store or pub-sub queue where servers can publish and read changes is enough to get started. Server languages that handle concurrency poorly will hit limits on scaling SSE connections sooner.

Streaming Flow

Here’s the flow for any SSE setup with Datastar. (For a refresher on the client side, see Datastar Basics.)

Datastar SSE request flow between client and server A sequence diagram. 1: the client sends GET slash. 2: the server responds with HTML containing data-on:load="@get('/stream')". 3: the client sends GET slash stream. 4: the server responds with an event-stream and leaves the connection open. 5: the server sends a datastar-patch-elements event carrying the full page state. 6: the server keeps sending datastar-patch-elements events as state changes. 7: the client posts to slash update; the response carries no state because the stream does. Steps 5 through 7 repeat for the life of the stream. Client Server stream stays open 1 GET / plain page request — no SSE yet 2 200 OK · text/html markup contains data-on:load="@get('/stream')" 3 GET /stream Datastar-Request: true 4 200 OK · text/event-stream headers only — the connection is left open 5 event: datastar-patch-elements the full state of the page 6 event: datastar-patch-elements … deltas, pushed as state changes 7 POST /update … changes come back on the stream, not in this response
  1. Client requests a server-rendered page that isn’t SSE.
  2. Server returns a page with an element that opens a Datastar SSE endpoint, like data-on:load="@get('/stream')". That can be the same path, if the server reads the Datastar-Request: true header to tell that Datastar initiated the request. This example uses a separate endpoint for clarity.
  3. Client requests the endpoint that provides the SSE stream.
  4. Server responds with headers indicating an SSE stream, leaving the connection open.
  5. Server sends the current page’s contents (again). This should be the full state of the page.
  6. Server sends deltas as needed. To get started, it’s fine to send the entire page’s contents again.
  7. The client may send mutations via request actions. Those changes come back on the stream, not in the response to the action.

Is #5 important? It’s what covers reconnects, which clients do arbitrarily.

SSE connections are browser-controlled: the browser may cut the connection at any point, and reconnect at any point. So whenever a connection is established, assume a disconnect preceded it and send the latest state. Steps 3-5 should repeat every time the browser reconnects.

A browser will typically drop an SSE connection when:

  • The user switches away from the tab using it
  • The browser is backgrounded (on mobile devices)
  • The connection idles (Datastar can keep-alive the response as a best effort)

This changes from browser version to browser version, as each vendor decides what’s optimal for battery life.

Sending the full state initially also handles a patch-elements edge case: navigating to a new page and then going back to the SSE-enabled one. Some browsers restore the HTML from the initial page load rather than the state Datastar patched in. Re-sending the state on connect resolves that too.

You can replace patch elements with patch signals, as long as signals cover every state change you’d otherwise make through SSE.

Backend Implementation Needs

SSE holds a connection open for a long time, so use a framework or language that supports IO concurrency (node, go, rust, etc.).

The traditional options for server-to-server communication all apply:

Whatever writes you make should publish the entity ID or page identifier that changed. A server receiving an SSE stream request then subscribes to those entities, and gets notified when changes occur on that page.

Use a modern compression for the SSE stream (zstd, brotli, etc.). The Datastar SDKs typically do this for you, but remember to if you’re implementing it yourself. This one change dramatically reduces the redundant data you send down the stream — including the full page updates you send (because we’re being lazy).

Some Other Uses for SSE Streaming

Some fun ideas that you can use the above process for:

Render Above the Fold, Stream in the Rest

The initial page render and the SSE stream don’t have to return the same results. The server can return only what’s above the fold — by limiting how many items a list returns, or by marking the elements to load later with an HTML element the server recognizes.

If the page doesn’t need real-time streaming, the server can close the stream once it has written the full page contents.

Multiplayer Collaboration

There is nothing more to do here! If you’ve implemented the design above, this already works.

Throttling

Updates are controlled by the server, so any updates can be rate limited by the server.