Skip to main content
Technology & EngineeringHtmx239 lines

Htmx Websockets

Real-time updates with the HTMX WebSocket extension and server-sent events

Quick Summary30 lines
You are an expert in implementing real-time, bidirectional communication in hypermedia-driven applications using the HTMX WebSocket extension and server-sent events (SSE).

## Key Points

- `ws-connect="/path"` — establishes a WebSocket connection
- `ws-send` — marks a form or element to send messages over the socket
- Messages from the server are HTML fragments that HTMX swaps into the DOM using standard targeting and swap rules
- `sse-connect="/path"` — opens an SSE connection
- `sse-swap="event-name"` — swaps content when the named event arrives
- Multiple elements can listen for different event types on the same connection
- **Send HTML fragments, not JSON.** Follow the HTMX philosophy. The server should render HTML and push it to clients. This eliminates client-side templating logic.
- **Use out-of-band swaps for multi-region updates.** A single WebSocket message can update multiple DOM targets using `hx-swap-oob`, keeping the protocol simple.
- **Implement server-side heartbeats.** Send a periodic comment line (`: heartbeat`) in SSE or a ping frame in WebSockets to keep the connection alive through proxies and load balancers.
- **Forgetting to load the extension.** The `ws` and `sse` extensions are not bundled with core HTMX. You must include the extension script and activate it with `hx-ext="ws"` or `hx-ext="sse"`.

## Quick Example

```json
{"message": "Hi Alice!", "HEADERS": {"HX-Request": "true"}}
```

```html
<div id="chat-messages" hx-swap-oob="beforeend">
  <div class="message">
    <strong>Bob:</strong> Hi Alice!
  </div>
</div>
```
skilldb get htmx-skills/Htmx WebsocketsFull skill: 239 lines
Paste into your CLAUDE.md or agent config

WebSocket Extension — HTMX

You are an expert in implementing real-time, bidirectional communication in hypermedia-driven applications using the HTMX WebSocket extension and server-sent events (SSE).

Overview

HTMX provides two extensions for real-time server communication: the WebSocket extension (ws) for bidirectional messaging and the SSE extension (sse) for server-to-client streaming. Both follow the HTMX philosophy of returning HTML fragments from the server that are swapped directly into the DOM. This avoids the typical pattern of receiving JSON over a socket and manually building DOM elements in JavaScript.

Core Concepts

WebSocket Extension (ws)

The ws extension manages the WebSocket lifecycle (connect, reconnect, message handling) declaratively via HTML attributes:

  • ws-connect="/path" — establishes a WebSocket connection
  • ws-send — marks a form or element to send messages over the socket
  • Messages from the server are HTML fragments that HTMX swaps into the DOM using standard targeting and swap rules

SSE Extension (sse)

The sse extension consumes server-sent events:

  • sse-connect="/path" — opens an SSE connection
  • sse-swap="event-name" — swaps content when the named event arrives
  • Multiple elements can listen for different event types on the same connection

Automatic Reconnection

Both extensions automatically reconnect on connection loss using an exponential backoff strategy, making them resilient to transient network issues.

Implementation Patterns

Real-Time Chat with WebSockets

<!-- Load the extension -->
<script src="https://unpkg.com/htmx-ext-ws@2.0.0/ws.js"></script>

<div hx-ext="ws" ws-connect="/chat/room/42">
  <!-- Message display area -->
  <div id="chat-messages">
    <!-- Existing messages rendered server-side -->
    <div class="message">
      <strong>Alice:</strong> Hello everyone!
    </div>
  </div>

  <!-- Send form -->
  <form ws-send>
    <input name="message" placeholder="Type a message..."
           autocomplete="off" required>
    <button type="submit">Send</button>
  </form>
</div>

When the form is submitted, HTMX serializes it as JSON and sends it over the WebSocket:

{"message": "Hi Alice!", "HEADERS": {"HX-Request": "true"}}

The server processes the message and broadcasts an HTML fragment to all connected clients:

<div id="chat-messages" hx-swap-oob="beforeend">
  <div class="message">
    <strong>Bob:</strong> Hi Alice!
  </div>
</div>

The hx-swap-oob="beforeend" tells HTMX to append the new message to #chat-messages on every connected client.

Live Notifications with SSE

<script src="https://unpkg.com/htmx-ext-sse@2.0.0/sse.js"></script>

<div hx-ext="sse" sse-connect="/notifications/stream">
  <!-- Notification badge updates -->
  <span id="notif-count"
        sse-swap="notification-count"
        hx-swap="innerHTML">
    0
  </span>

  <!-- Toast notifications -->
  <div id="toast-container"
       sse-swap="new-notification"
       hx-swap="afterbegin">
  </div>
</div>

Server sends events:

event: notification-count
data: <span>3</span>

event: new-notification
data: <div class="toast" role="alert">You have a new message from Alice.</div>

Live Dashboard Updates

<div hx-ext="sse" sse-connect="/dashboard/stream">
  <div class="metric-card">
    <h3>Active Users</h3>
    <div id="active-users" sse-swap="active-users" hx-swap="innerHTML">
      Loading...
    </div>
  </div>

  <div class="metric-card">
    <h3>Orders Today</h3>
    <div id="orders-today" sse-swap="orders-today" hx-swap="innerHTML">
      Loading...
    </div>
  </div>

  <div class="metric-card">
    <h3>Revenue</h3>
    <div id="revenue" sse-swap="revenue" hx-swap="innerHTML">
      Loading...
    </div>
  </div>
</div>

Server sends periodic updates:

event: active-users
data: <span class="metric">1,247</span>

event: orders-today
data: <span class="metric">384</span>

event: revenue
data: <span class="metric">$42,850</span>

WebSocket with Out-of-Band Updates

The server can update multiple parts of the page in a single WebSocket message:

<div hx-ext="ws" ws-connect="/game/lobby">
  <div id="player-list">
    <!-- players listed here -->
  </div>
  <div id="game-status">Waiting for players...</div>
  <div id="chat-log"></div>
</div>

Server broadcasts a single message containing multiple OOB fragments:

<div id="player-list" hx-swap-oob="innerHTML">
  <div class="player">Alice (Ready)</div>
  <div class="player">Bob (Not Ready)</div>
  <div class="player">Charlie (Ready)</div>
</div>
<div id="game-status" hx-swap-oob="innerHTML">
  3 players connected — waiting for all to be ready
</div>
<div id="chat-log" hx-swap-oob="beforeend">
  <p><em>Charlie has joined the lobby.</em></p>
</div>

Connection Status Indicator

<div hx-ext="ws" ws-connect="/updates"
     id="ws-container">
  <span id="connection-status" class="connected">Connected</span>
  <div id="content">...</div>
</div>

<script>
  document.body.addEventListener("htmx:wsOpen", function(evt) {
    document.getElementById("connection-status").textContent = "Connected";
    document.getElementById("connection-status").className = "connected";
  });

  document.body.addEventListener("htmx:wsClose", function(evt) {
    document.getElementById("connection-status").textContent = "Reconnecting...";
    document.getElementById("connection-status").className = "disconnected";
  });
</script>

Core Philosophy

Real-time communication in HTMX follows the same hypermedia principle as the rest of the library: the server sends HTML, and the client swaps it into the DOM. There are no JSON messages to parse, no client-side templates to maintain, and no state synchronization layer. A WebSocket message or SSE event contains rendered HTML fragments that HTMX inserts directly, keeping the rendering logic centralized on the server.

The choice between WebSockets and SSE is driven by the direction of communication, not by a preference for one technology over the other. SSE is simpler, works over standard HTTP, auto-reconnects natively, and is the right choice when the server pushes updates to the client. WebSockets add bidirectional messaging, which is necessary only when clients need to send data back (chat, collaborative editing, gaming). Choosing SSE when you only need server-to-client streaming avoids the operational complexity of managing WebSocket connections.

Both extensions embrace automatic reconnection with exponential backoff, which makes them resilient to transient network failures without any custom retry logic. However, reconnection creates a gap during which the client may miss updates. The architecture must account for this by either sending a full state snapshot on reconnect or implementing a catch-up mechanism. This is a server-side concern, not a client-side one, which aligns with HTMX's philosophy of keeping intelligence on the server.

Anti-Patterns

  • Sending JSON over WebSockets and building DOM manually in JavaScript. This defeats HTMX's purpose. If the server sends HTML fragments, HTMX handles all DOM insertion automatically. Sending JSON reintroduces client-side templating complexity.

  • Opening multiple SSE connections to the same domain. Browsers limit concurrent HTTP/1.1 connections per domain (typically six). Each SSE stream uses one. Too many streams can starve regular HTTP requests. Consolidate events into a single SSE endpoint or use HTTP/2.

  • Ignoring reconnection state. After a WebSocket or SSE reconnection, the client may have missed updates. Without a resynchronization mechanism, the UI silently becomes stale. Send a full state snapshot on connection or request one from the client.

  • Accumulating DOM content indefinitely in live feeds. Chat messages, log entries, and notification lists grow without bound. Without periodic trimming, the page accumulates thousands of DOM nodes, degrading rendering performance and consuming memory.

  • Forgetting to load the extension scripts. The ws and sse extensions are not part of core HTMX. Omitting the extension script and adding hx-ext="ws" produces no error but also no functionality, leading to silent failures that are hard to debug.

Best Practices

  • Use SSE for server-to-client streaming, WebSockets for bidirectional messaging. SSE is simpler, works over standard HTTP, and auto-reconnects natively. Only use WebSockets when clients need to send messages back.
  • Send HTML fragments, not JSON. Follow the HTMX philosophy. The server should render HTML and push it to clients. This eliminates client-side templating logic.
  • Use out-of-band swaps for multi-region updates. A single WebSocket message can update multiple DOM targets using hx-swap-oob, keeping the protocol simple.
  • Namespace your SSE event types. Use descriptive event names like user-joined, order-updated rather than generic names. This makes the code self-documenting and allows different elements to listen for specific events.
  • Implement server-side heartbeats. Send a periodic comment line (: heartbeat) in SSE or a ping frame in WebSockets to keep the connection alive through proxies and load balancers.
  • Scope WebSocket connections. Place the ws-connect attribute on the smallest relevant container. When that element is removed from the DOM (e.g., by navigating away), HTMX automatically closes the connection.

Common Pitfalls

  • Forgetting to load the extension. The ws and sse extensions are not bundled with core HTMX. You must include the extension script and activate it with hx-ext="ws" or hx-ext="sse".
  • Sending HTML in SSE data fields with newlines. SSE data fields cannot contain raw newlines. Either send single-line HTML or use multiple data: lines (they are concatenated with newlines by the browser).
  • Not handling reconnection state. After reconnection, the client may have missed updates. Implement a strategy to resynchronize: the server can send a full state snapshot on connection, or the client can request one.
  • Opening too many SSE connections. Browsers limit concurrent HTTP connections per domain (typically 6 for HTTP/1.1). Each SSE stream uses one connection. Use HTTP/2 (which multiplexes streams) or consolidate events into a single SSE endpoint.
  • Memory leaks from accumulated DOM content. In a chat or live feed, messages accumulate indefinitely. Periodically trim old messages on the client side using an htmx:afterSwap handler or limit the initial history sent by the server.

Install this skill directly: skilldb add htmx-skills

Get CLI access →