Long Polling Β· Astro Tech Blog

Long Polling

Long polling is a technique for receiving real-time updates when WebSockets aren’t available. The client makes a request, and the server holds the response until new data is available.

How It Works

Client              Server
  β”‚                    β”‚
  β”œβ”€β”€ GET /updates ──▢│  (request sent)
  β”‚                    β”œβ”€β”€ waits for new data...
  β”‚                    β”‚   (connection stays open)
  β”‚                    β”‚
  │◀── response ───────  (data available β€” response sent)
  β”‚                    β”‚
  β”œβ”€β”€ GET /updates ──▢│  (immediately re-connect)
  β”‚                    β”œβ”€β”€ waits...
  β”‚                    β”‚
  │◀── response ───────
  β”‚                    β”‚
  └── (repeats) β”€β”€β”€β”€β”€β”€β”˜

Basic Long Polling Client

async function longPoll(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    // Process the update
    onData(data);
  } catch (err) {
    // Connection error β€” wait and retry
    await wait(1000);
  } finally {
    // Immediately reconnect
    longPoll(url);
  }
}

// Start polling
longPoll('/api/updates');
Demo: Long Polling Demo
HTML
<div>
<p>Simulated server sends updates at random intervals.</p>
<button id='start-poll'>Start Long Polling</button>
<button id='stop-poll' disabled>Stop</button>
<div style='margin-top:8px;max-height:200px;overflow-y:auto;background:#f8fafc;border:1px solid #e2e8f0;border-radius:6px;padding:8px;'>
<div id='poll-messages' style='font-family:monospace;font-size:0.875rem;'></div>
</div>
<pre id='poll-status' style='background:#f1f5f9;padding:8px;border-radius:4px;margin-top:8px;'></pre>
</div>
JavaScript
const messages = document.getElementById('poll-messages');
const status = document.getElementById('poll-status');
let active = false;
let pollId = 0;
let messageCount = 0;

// Simulated server that holds responses until data is ready
function simulatedFetch(url, signal) {
return new Promise((resolve, reject) => {
const delay = 500 + Math.random() * 3000;

const timer = setTimeout(() => {
if (signal?.aborted) {
reject(new DOMException('Aborted', 'AbortError'));
return;
}
messageCount++;
resolve({
ok: true,
json: async () => ({
id: messageCount,
message: 'Update #' + messageCount + ' at ' + new Date().toLocaleTimeString(),
timestamp: Date.now()
})
});
}, delay);

if (signal) {
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
});
}
});
}

async function longPoll() {
if (!active) return;
const currentPollId = pollId;
status.textContent = 'πŸ”„ Long polling... waiting for updates';

try {
const controller = new AbortController();
// Store controller so we can abort on stop
window.__pollController = controller;

const response = await simulatedFetch('/updates', controller.signal);
const data = await response.json();

if (!active || currentPollId !== pollId) return;

const div = document.createElement('div');
div.textContent = 'πŸ“© ' + data.message;
messages.insertBefore(div, messages.firstChild);
status.textContent = 'βœ… Received update #' + data.id + ' β€” reconnecting...';

} catch (err) {
if (err.name === 'AbortError') return;
status.textContent = '❌ Error, retrying in 1s...';
await new Promise(r => setTimeout(r, 1000));
}

if (active) longPoll();
}

document.getElementById('start-poll').onclick = function() {
active = true;
pollId++;
messageCount = 0;
messages.innerHTML = '';
this.disabled = true;
document.getElementById('stop-poll').disabled = false;
status.textContent = 'Long polling started';
longPoll();
};

document.getElementById('stop-poll').onclick = function() {
active = false;
pollId++;
if (window.__pollController) {
window.__pollController.abort();
}
this.disabled = true;
document.getElementById('start-poll').disabled = false;
status.textContent = '⏹️ Long polling stopped';
};
Live Output Window

Advantages and Disadvantages

ProsCons
Works everywhere (no special server)Higher latency than WebSocket
Firewall-friendly (standard HTTP)Server overhead (many open connections)
Simple to implementMessages may be delayed (TTFB)
Automatic reconnectionNot suitable for high-frequency updates

Server-Side (Node.js Example)

// Server-side long polling endpoint
app.get('/updates', async (req, res) => {
  // Wait until there's new data or timeout
  const data = await waitForNewData(30000); // 30s timeout

  if (data) {
    res.json(data);
  } else {
    res.status(204).end(); // no content β€” client will reconnect
  }
});

// Event emitter pattern
const EventEmitter = require('events');
const updates = new EventEmitter();

function waitForNewData(timeout) {
  return new Promise((resolve) => {
    const timer = setTimeout(() => resolve(null), timeout);

    updates.once('newData', (data) => {
      clearTimeout(timer);
      resolve(data);
    });
  });
}

Long Polling vs WebSocket

Long PollingWebSocket
HTTP requests (stateless)Persistent TCP connection
Higher latencyLower latency
Reconnects automaticallyNeeds reconnection logic
Works through all proxiesSome proxies may block
Simpler to debugHarder to debug
Good for low-frequency updatesGood for high-frequency

Connection Management

  • Always set a timeout (30-60s) on the server side
  • Handle network errors gracefully β€” reconnect with backoff
  • Use a unique request ID to prevent duplicate processing
  • Consider multiple parallel polls for reliability

Key Takeaways

  • Long polling keeps a connection open until data is available
  • The client immediately reconnects after receiving a response
  • Good for low-frequency real-time updates (notifications, chat)
  • Works with any HTTP server β€” no special protocol needed
  • Handle errors with retry and backoff
  • For high-frequency updates, prefer WebSockets or Server-Sent Events
  • Always implement a timeout to prevent hanging connections