htmx: Build Interactive Web Apps with HTML · Beyond the Basics
Real-Time Updates
Choose between polling, Server-Sent Events and WebSockets, and build an export job whose progress updates live.
What you'll build
An export contacts job: pressing the button starts a job on the server, and a progress area updates live until the job finishes, then stops asking.
The idea
| Technique | How | Choose it when |
|---|---|---|
| Polling | hx-trigger="every 2s"; status 286 stops it |
updates can be a few seconds late; simplest to run |
| Server-Sent Events | htmx-ext-sse: sse-connect, sse-swap |
the server pushes updates as they happen (one-way) |
| WebSockets | htmx-ext-ws: ws-connect, ws-send |
both sides talk constantly (chat, collaboration) |
Polling works through every proxy and load balancer and is easy to scale; SSE and WebSockets keep a connection open per user, which needs care with timeouts and server capacity.
The code
<div id="export">
<button hx-post="/exports" hx-target="#export">Export contacts (CSV)</button>
</div>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
let progress = 0;
server.post("/exports", () => {
progress = 0; // the job starts; the response contains the element that polls
return `<p hx-get="/exports/17" hx-trigger="every 1s">Export queued...</p>`;
});
server.get("/exports/17", () => {
progress += 25;
if (progress >= 100) return { status: 286, body: `<a href="#">contacts.csv</a> is ready.` };
return `<progress value="${progress}" max="100"></progress> ${progress}%`;
});
</script>
The same feature with SSE pushes each step instead of being asked:
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<div hx-ext="sse" sse-connect="/exports/17/events" sse-swap="progress" sse-close="done">Export queued...</div>
Common mistakes
- Polling with no stop condition: finished jobs keep generating requests. Return 286 or replace the polling element.
- Holding database connections open per SSE/WebSocket client: use async servers or a pub/sub layer.
- Choosing WebSockets for one-way notifications: SSE is simpler and reconnects automatically.
Key points
- Start with polling; move to SSE when latency matters, WebSockets when traffic is two-way.
- 286 stops htmx polling.
- htmx 2 provides SSE and WebSockets as separate extensions.
Further reading: htmx SSE extension · htmx WebSocket extension · htmx example: progress bar
Practice questions
How does a server stop an htmx polling element?
Answer
Respond with HTTP status 286, or return content without the polling trigger.
When would you choose SSE over WebSockets?
Answer
For one-way server-to-browser updates: SSE is plain HTTP, simpler to operate and reconnects automatically.
What's the operational cost of SSE and WebSockets compared with polling?
Answer
Each client holds an open connection, so servers, proxies and load balancers must handle many long-lived connections and timeouts.
Open in the app to mark lessons complete, take notes and track your progress.