HTMX Polling

Polling means sending a request to the server on a fixed time interval to check for new data. It is the simplest way to build a live-updating UI — a sports score ticker, a server health dashboard, a queue status page, or a notification counter. HTMX builds polling into the trigger system with one small addition: the every keyword.

When to Use Polling

Use CasePolling Good?Reason
Sports score every 5 secondsYesLow frequency, acceptable overhead
Server CPU dashboard every 2 secondsYesNear-real-time is good enough
Chat messages every 500msNoToo frequent — use WebSockets or SSE instead
Order status every 10 secondsYesInfrequent changes, simple to implement
Collaborative editing cursor positionsNoNeeds sub-second updates — use WebSockets

Basic Polling Syntax

<div
  hx-get="/live-score"
  hx-trigger="every 5s"
  hx-target="this"
  hx-swap="innerHTML">
  Loading score...
</div>

HTMX sends GET /live-score every 5 seconds. The server returns the latest score HTML and HTMX replaces the div's content. The interval runs continuously until the element is removed from the DOM or the user leaves the page.

  Timeline:

  0s   → GET /live-score → "Home 1 – Away 0"
  5s   → GET /live-score → "Home 1 – Away 0"   (no change)
  10s  → GET /live-score → "Home 2 – Away 0"   (goal scored!)
  15s  → GET /live-score → "Home 2 – Away 1"
  ...

Polling Interval Units

SyntaxInterval
every 1sEvery 1 second
every 30sEvery 30 seconds
every 2mEvery 2 minutes
every 500msEvery 500 milliseconds

Combining Polling With Load Trigger

Start polling immediately when the element loads, and also fire once on load to populate it right away:

<div
  hx-get="/notifications/count"
  hx-trigger="load, every 60s"
  hx-target="this">
  ...
</div>

The first load trigger fires once immediately. The every 60s trigger then keeps it refreshing once a minute.

Server-Driven Polling Stop

The server can stop the polling loop by returning the response header HX-Trigger: stop-polling along with the response. Pair this with a custom event listener on the element:

<div
  id="job-status"
  hx-get="/job/42/status"
  hx-trigger="every 3s"
  hx-target="this"
  hx-on:stop-polling="htmx.removeAttribute(this, 'hx-trigger')">
  Checking job status...
</div>

Server response when the job finishes:

# Flask
response = Response("<p>Job complete! ✓</p>")
response.headers['HX-Trigger'] = 'stop-polling'
return response
  Flow:

  0s  → GET /job/42/status → "Processing... (20%)"
  3s  → GET /job/42/status → "Processing... (60%)"
  6s  → GET /job/42/status → "Processing... (90%)"
  9s  → GET /job/42/status → "Job complete! ✓"
                             + HX-Trigger: stop-polling
       |
       v
  stop-polling event fires
  hx-trigger attribute removed from element
  Polling stops — no more requests

Conditional Polling Based on Response

Another approach is to have the server return a new element with or without the polling trigger, depending on the current state:

# Flask
@app.route('/job/<int:job_id>/status')
def job_status(job_id):
    job = Job.query.get(job_id)

    if job.status == 'complete':
        # Return element WITHOUT polling trigger — polling stops naturally
        return f'''
        <div id="job-status" hx-swap-oob="true">
          <p>Job complete! ✓</p>
        </div>
        '''
    else:
        # Return element WITH polling trigger — polling continues
        return f'''
        <div id="job-status"
             hx-get="/job/{job_id}/status"
             hx-trigger="every 3s"
             hx-target="this"
             hx-swap="outerHTML">
          Processing... ({job.progress}%)
        </div>
        '''

When the job is complete, the server returns a div without hx-trigger. HTMX replaces the polling element with the finished-state element, and polling stops because the new element has no trigger.

Full Dashboard Polling Example

<h2>System Dashboard</h2>

<div hx-get="/stats/cpu"    hx-trigger="load, every 2s" hx-target="this">CPU: --</div>
<div hx-get="/stats/memory" hx-trigger="load, every 5s" hx-target="this">RAM: --</div>
<div hx-get="/stats/disk"   hx-trigger="load, every 30s" hx-target="this">Disk: --</div>

Each stat polls at its own interval. CPU updates every 2 seconds (fast-changing), disk every 30 seconds (slow-changing). Each element manages its own polling loop independently.

Polling and Server Load

Each polling element sends a request on every interval. With 1,000 visitors and three polling elements each firing every 5 seconds, your server receives 600 requests per second just for polling. Keep these points in mind:

  • Use the longest interval that still feels live for your use case.
  • Cache polling responses on the server when the data changes less often than the polling interval.
  • Consider Server-Sent Events (SSE) for high-frequency updates — the server pushes data only when it changes, eliminating wasted requests.
  • Stop polling for hidden tabs using the Page Visibility API in JavaScript.

Stopping Polling When Tab Is Hidden

<script>
document.addEventListener('visibilitychange', function() {
    const poller = document.getElementById('live-score');
    if (document.hidden) {
        // Remove trigger to pause polling
        poller.removeAttribute('hx-trigger');
    } else {
        // Restore trigger to resume polling
        poller.setAttribute('hx-trigger', 'every 5s');
        htmx.process(poller);   // Re-initialize HTMX on the element
    }
});
</script>

Key Takeaway

HTMX polling uses the every Xs trigger syntax to send requests on a fixed interval. It is the simplest path to live-updating content. Stop polling by removing the hx-trigger attribute, by having the server return a response without a polling trigger, or by sending the HX-Trigger: stop-polling response header. Keep intervals as long as practical to minimize unnecessary server load, and pause polling when the tab is hidden to save both server and client resources.

Leave a Comment

Your email address will not be published. Required fields are marked *