WebSockets and Real-Time Communication

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.

Choose the Transport by Interaction Pattern

Use ordinary HTTP when the client asks for data occasionally and latency is not critical.

Use server-sent events (SSE) when:

  • the browser mainly needs a one-way stream from server to client
  • the client can still send commands over normal HTTP
  • the protocol should stay simple and text-oriented

Use WebSockets when:

  • the client and server both initiate messages
  • latency and interaction frequency justify a persistent connection
  • session state, presence, collaboration, trading, multiplayer behavior, or live control traffic make request/response too awkward

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’s Current Fit

Sente is still a strong higher-level choice when you want one API over WebSockets plus Ajax fallback. Its current README emphasizes:

  • bidirectional async communication
  • automatic reconnect and keep-alive behavior
  • protocol selection and Ajax fallback
  • a unified event model on top of WebSockets and Ajax
  • tracking which users are connected and on which transports

That makes it especially useful for browser-facing systems where transport flexibility matters more than owning the lowest-level socket details yourself.

A Minimal Sente Shape

 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.

Connection Lifecycle Is Part of the Design

Good realtime systems define what happens when the connection:

  • first opens
  • loses connectivity
  • reconnects with stale client state
  • sends a duplicate command
  • subscribes before authorization is confirmed
  • becomes idle but still consumes resources

Treat these as protocol questions, not UI cleanup. The connection lifecycle often determines whether the feature is trustworthy under production conditions.

Real-Time Messaging Needs Policy, Not Just a Socket

Define the message contract up front:

  • event names and payload shape
  • authentication and re-authentication rules
  • idempotency or deduplication strategy
  • fan-out rules by room, tenant, or user
  • heartbeat and idle timeout behavior
  • maximum payload size
  • slow-consumer handling

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.

WebSockets Are Not the Only Realtime Tool

Choose SSE or polling instead when:

  • the browser only needs server-to-client updates
  • updates can be coalesced
  • simpler infrastructure is worth more than bidirectional messaging
  • most writes already happen through ordinary HTTP APIs

That is a common improvement in dashboards and status views. Not every live page needs a socket.

Security and Scaling Questions

For production use, review:

  • wss:// everywhere outside development
  • authentication before subscription or room join
  • tenant-aware authorization on every pushed event path
  • connection limits and rate limits
  • sticky sessions or shared state strategy when multiple nodes serve the same users
  • observability for open connections, message rate, reconnect rate, and dropped messages

Key Takeaways

  • Choose WebSockets only when the interaction is genuinely bidirectional and latency-sensitive.
  • Prefer SSE or ordinary HTTP when the problem is simpler than a persistent socket.
  • Sente remains a strong option when you want WebSocket plus Ajax fallback behind one event-oriented API.
  • Design reconnection, deduplication, authorization, and slow-consumer policy explicitly.
  • Treat realtime transport as part of system design, not just as a UI feature.

References and Further Reading

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026