How to choose WebSockets versus SSE or polling in Clojure systems, with current Sente guidance and operational trade-offs.
WebSockets are the right tool when both client and server need a long-lived, low-latency, bidirectional channel. They are not the default answer for every page that needs “live updates.” A good design starts by choosing the interaction model first, then picking the transport that matches it.
That matters because realtime systems fail in very specific ways: reconnect storms, duplicate delivery, unauthenticated subscriptions, oversized fan-out, and message rates that outpace what a browser tab can actually process.
Use ordinary HTTP when the client asks for data occasionally and latency is not critical.
Use server-sent events (SSE) when:
Use WebSockets when:
One practical limit is worth remembering: the browser WebSocket API does not give you real backpressure. If messages arrive faster than the UI can consume them, your system needs its own throttling, coalescing, or drop policy.
Sente is still a strong higher-level choice when you want one API over WebSockets plus Ajax fallback. Its current README emphasizes:
That makes it especially useful for browser-facing systems where transport flexibility matters more than owning the lowest-level socket details yourself.
1(ns myapp.realtime
2 (:require [clojure.core.async :as a]
3 [taoensso.sente :as sente]
4 [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]]))
5
6(let [{:keys [ch-recv
7 send-fn
8 connected-uids
9 ajax-post-fn
10 ajax-get-or-ws-handshake-fn]}
11 (sente/make-channel-socket! (get-sch-adapter) {})]
12 (def ring-ajax-post ajax-post-fn)
13 (def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn)
14 (def chsk-send! send-fn)
15 (def connected-uids-state connected-uids)
16
17 (a/go-loop []
18 (when-let [{:keys [event uid]} (a/<! ch-recv)]
19 (case (first event)
20 :chat/send
21 (doseq [connected-uid (:any @connected-uids-state)]
22 (chsk-send! connected-uid [:chat/message {:from uid
23 :text (second event)}]))
24 nil)
25 (recur))))
This example matters for the architecture it implies: clients exchange event vectors, the server normalizes them, and broadcast policy stays explicit in the event loop.
sequenceDiagram
participant Browser
participant Transport
participant Sente
participant App
Browser->>Transport: Connect
Transport-->>Browser: WebSocket or Ajax fallback
Browser->>Sente: Event message
Sente->>App: Normalized event
App->>Sente: Targeted push or broadcast
Sente->>Browser: Outbound event
The transport is deliberately not the main abstraction. The application-level event model is.
Good realtime systems define what happens when the connection:
Treat these as protocol questions, not UI cleanup. The connection lifecycle often determines whether the feature is trustworthy under production conditions.
Define the message contract up front:
The browser API’s weak backpressure story means you should expect bursts and build a policy for them. Sometimes the right answer is to collapse many updates into a single “latest state” event instead of trying to deliver every intermediate change.
Choose SSE or polling instead when:
That is a common improvement in dashboards and status views. Not every live page needs a socket.
For production use, review:
wss:// everywhere outside development