Loading content...
Long polling often receives unfair criticism as a "legacy" technology, superseded by WebSockets and SSE. This perspective fundamentally misunderstands what long polling offers. It's not a compromise—it's a deliberate architectural choice with specific advantages.
In this concluding page, we'll establish clear criteria for when long polling is the optimal choice. You'll learn to recognize scenarios where long polling outperforms seemingly "modern" alternatives, and you'll see how major companies continue to rely on it for critical systems.
The goal isn't to convince you that long polling is always right—it's to equip you with the judgment to recognize when it is right, and the confidence to choose it despite assumptions about newer technologies being inherently better.
By the end of this page, you'll have concrete decision criteria for choosing long polling, understand the specific scenarios where it excels, and see real-world examples of long polling in production systems. You'll be able to confidently justify long polling as a technology choice when appropriate.
Before examining specific use cases, let's establish definitive criteria for when long polling should be your choice.
Choose Long Polling When:
Avoid Long Polling When:
Approximately 90% of real-time requirements can be served equally well by long polling, SSE, or WebSocket. The latency differences don't meaningfully impact user experience. Choose based on secondary factors: compatibility, simplicity, team expertise, and operational considerations.
Enterprise environments present unique networking challenges that make long polling particularly valuable.
The Corporate Network Reality:
Corporate networks are designed for security and control, not developer convenience. They typically feature:
| Protocol | Typical Success Rate | Common Failure Mode |
|---|---|---|
| HTTP (short/long poll) | 99%+ | Rarely fails (standard HTTP) |
| SSE | 85-95% | Proxy streaming limits |
| WebSocket | 60-85% | Upgrade blocked, proxy doesn't understand |
| WebSocket (WSS) | 75-90% | TLS inspection breaks handshake |
Real Example: Enterprise SaaS Deployment
Consider a SaaS application deployed to Fortune 500 clients:
Client A: Standard corporate network
- WebSocket: Works
Client B: Financial institution
- WebSocket: Blocked by compliance proxy
Client C: Government agency
- WebSocket: Blocked by security policy
Client D: Healthcare organization
- WebSocket: Works intermittently (proxy restarts)
Client E: Manufacturing company
- WebSocket: Blocked (legacy proxy)
With WebSocket-only architecture, 60% of enterprise clients experience issues. With long polling fallback or long polling as primary, 100% of clients work reliably.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// Configuration optimized for enterprise network constraints const enterpriseLongPollConfig = { // Shorter timeout to stay under aggressive proxy limits serverTimeout: 25000, // 25 seconds (many proxies cut at 30) // Immediate reconnect to minimize gaps reconnectDelay: 0, // Aggressive retry for transient proxy issues maxRetries: 10, baseBackoff: 1000, maxBackoff: 10000, // Headers for proxy compatibility headers: { // Prevent caching through enterprise caches 'Cache-Control': 'no-cache, no-store, must-revalidate, private', 'Pragma': 'no-cache', 'Expires': '0', // Hint to proxies this is a long-lived request 'X-Accel-Buffering': 'no', // nginx-specific // Some proxies treat XHR differently 'X-Requested-With': 'XMLHttpRequest', }, // Probe endpoint to detect corporate proxy behavior probeConfig: { enabled: true, endpoint: '/api/network-probe', interval: 300000, // Re-probe every 5 minutes },}; // Network probe to detect and adapt to corporate restrictionsclass NetworkProbe { async probe(): Promise<NetworkEnvironment> { const results: NetworkEnvironment = { maxHoldTime: await this.probeMaxHoldTime(), supportsKeepAlive: await this.probeKeepAlive(), proxyDetected: await this.detectProxy(), }; return results; } private async probeMaxHoldTime(): Promise<number> { // Try increasingly long requests to find the proxy cutoff const testDurations = [10, 20, 30, 45, 60]; // seconds let maxSuccessful = 5; for (const duration of testDurations) { try { const response = await fetch(`/api/probe?hold=${duration}`, { signal: AbortSignal.timeout((duration + 5) * 1000), }); if (response.ok) { maxSuccessful = duration; } else { break; // Timeout hit } } catch { break; // Connection cut } } // Use 80% of max to be safe return maxSuccessful * 0.8 * 1000; }}Consumer tech companies often overlook enterprise networking constraints because their developers work on modern networks. When these companies sell to enterprises, they discover WebSocket failures. Long polling as a primary or fallback transport prevents these sales-blocking issues.
Notification systems are perhaps the ideal use case for long polling. The requirements align perfectly with long polling's strengths.
Why Notifications Suit Long Polling:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
// Complete notification system using long polling class NotificationService { private longPoll: LongPollClient; private unreadCount = 0; private lastNotificationId: string | null = null; constructor() { this.longPoll = new LongPollClient({ endpoint: '/api/notifications/poll', getCursor: () => this.lastNotificationId, onEvents: (notifications) => { this.handleNotifications(notifications); }, onError: (error) => { // Silent failure for notifications - not critical console.warn('Notification polling error:', error); }, }); } start() { // Initial load of unread notifications this.loadUnread(); // Start long polling for new notifications this.longPoll.start(); // Handle visibility changes - pause when tab hidden document.addEventListener('visibilitychange', () => { if (document.hidden) { this.longPoll.pause(); } else { this.longPoll.resume(); } }); } private async loadUnread() { const response = await fetch('/api/notifications/unread'); const { notifications, count } = await response.json(); this.unreadCount = count; this.updateBadge(); if (notifications.length > 0) { this.lastNotificationId = notifications[0].id; } } private handleNotifications(notifications: Notification[]) { for (const notification of notifications) { // Update cursor this.lastNotificationId = notification.id; // Update badge this.unreadCount++; this.updateBadge(); // Show toast notification this.showToast(notification); // Browser notification if permitted if (notification.priority === 'high') { this.showBrowserNotification(notification); } // Update notification list if visible this.appendToList(notification); } } private updateBadge() { const badge = document.querySelector('.notification-badge'); if (badge) { badge.textContent = this.unreadCount > 99 ? '99+' : String(this.unreadCount); badge.classList.toggle('hidden', this.unreadCount === 0); } // Update favicon badge (if supported) this.updateFaviconBadge(this.unreadCount); } private showToast(notification: Notification) { // UI toast notification const toast = document.createElement('div'); toast.className = 'notification-toast'; toast.innerHTML = ` <div class="toast-icon">${this.getIcon(notification.type)}</div> <div class="toast-content"> <div class="toast-title">${notification.title}</div> <div class="toast-body">${notification.body}</div> </div> `; document.body.appendChild(toast); // Auto-dismiss after 5 seconds setTimeout(() => toast.remove(), 5000); } private async showBrowserNotification(notification: Notification) { if (Notification.permission === 'granted') { new Notification(notification.title, { body: notification.body, icon: '/notification-icon.png', tag: notification.id, // Prevents duplicates }); } }} // Server-side handlerapp.get('/api/notifications/poll', async (req, res) => { const userId = req.user.id; const since = req.query.since as string | undefined; const timeout = 30000; // Check for notifications newer than cursor const notifications = await db.notifications.findMany({ where: { userId, createdAt: since ? { gt: new Date(since) } : undefined, }, orderBy: { createdAt: 'asc' }, take: 20, }); if (notifications.length > 0) { return res.json({ notifications }); } // Wait for new notifications const listener = (notification: Notification) => { clearTimeout(timer); cleanup(); res.json({ notifications: [notification] }); }; const timer = setTimeout(() => { cleanup(); res.json({ notifications: [] }); }, timeout); const cleanup = () => { notificationBus.off(`user:${userId}`, listener); }; notificationBus.once(`user:${userId}`, listener); req.on('close', () => { clearTimeout(timer); cleanup(); });});Pause long polling when the browser tab is hidden. There's no point consuming server resources for notifications the user can't see. Resume when the tab becomes visible. This can reduce server load by 30-50% depending on user behavior patterns.
Real-time dashboards displaying metrics, analytics, and system status are excellent candidates for long polling. The update frequency is typically measured in seconds, and reliability matters more than sub-second latency.
Dashboard Characteristics That Favor Long Polling:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
// Dashboard metrics streaming via long polling interface DashboardMetrics { serverCount: number; activeUsers: number; requestsPerSecond: number; errorRate: number; p99Latency: number; alerts: Alert[]; lastUpdated: number;} class DashboardLongPoll { private pollClient: LongPollClient; private currentMetrics: DashboardMetrics | null = null; private subscribers: Set<(metrics: DashboardMetrics) => void> = new Set(); constructor(private dashboardId: string) { this.pollClient = new LongPollClient({ endpoint: `/api/dashboards/${dashboardId}/poll`, getCursor: () => this.currentMetrics?.lastUpdated?.toString() || null, onEvents: (updates) => { this.processUpdates(updates); }, // Dashboard should never stop trying maxRetries: Infinity, // Report connection status onStateChange: (state) => { this.updateConnectionIndicator(state); }, }); } subscribe(callback: (metrics: DashboardMetrics) => void) { this.subscribers.add(callback); // Send current metrics immediately if available if (this.currentMetrics) { callback(this.currentMetrics); } return () => this.subscribers.delete(callback); } private processUpdates(updates: Partial<DashboardMetrics>[]) { for (const update of updates) { // Merge update into current metrics this.currentMetrics = { ...this.currentMetrics, ...update, } as DashboardMetrics; } // Notify all subscribers for (const subscriber of this.subscribers) { subscriber(this.currentMetrics!); } // Update DOM (for standalone dashboard displays) this.updateDOM(this.currentMetrics!); } private updateDOM(metrics: DashboardMetrics) { // Update metric cards document.getElementById('server-count')!.textContent = metrics.serverCount.toString(); document.getElementById('active-users')!.textContent = metrics.activeUsers.toLocaleString(); document.getElementById('rps')!.textContent = metrics.requestsPerSecond.toFixed(1); document.getElementById('error-rate')!.textContent = (metrics.errorRate * 100).toFixed(2) + '%'; document.getElementById('p99-latency')!.textContent = metrics.p99Latency + 'ms'; // Update alerts panel const alertsContainer = document.getElementById('alerts')!; alertsContainer.innerHTML = metrics.alerts.map(alert => ` <div class="alert alert-${alert.severity}"> <span class="alert-time">${this.formatTime(alert.timestamp)}</span> <span class="alert-message">${alert.message}</span> </div> `).join(''); // Update last refreshed timestamp const lastUpdated = new Date(metrics.lastUpdated); document.getElementById('last-updated')!.textContent = `Last updated: ${lastUpdated.toLocaleTimeString()}`; } private updateConnectionIndicator(state: string) { const indicator = document.getElementById('connection-status')!; indicator.className = `status-indicator status-${state}`; indicator.title = state === 'connected' ? 'Connected - receiving updates' : `${state} - reconnecting...`; }} // Server: Efficient incremental updatesclass DashboardPoller { private lastMetrics: Map<string, DashboardMetrics> = new Map(); async handlePoll(dashboardId: string, since: string | null): Promise<object> { const current = await this.computeMetrics(dashboardId); const last = since ? this.lastMetrics.get(dashboardId) : null; // Compute delta if (last && this.metricsEqual(current, last)) { // No change - return minimal response return { changed: false }; } // Store for next comparison this.lastMetrics.set(dashboardId, current); // Return only changed fields if (last) { const delta = this.computeDelta(last, current); return { changed: true, delta }; } // Full update (first poll) return { changed: true, full: current }; }}Incremental Updates Pattern:
For dashboards, sending only changed data reduces bandwidth significantly:
| Update Type | Payload Size | Bandwidth (100 clients, 5s interval) |
|---|---|---|
| Full metrics every poll | ~2KB | ~40 KB/s |
| Changed fields only | ~200B avg | ~4 KB/s |
| Compressed delta | ~50B avg | ~1 KB/s |
NOC Display Considerations:
Network Operations Center displays run 24/7 on dedicated monitors. Long polling is ideal because:
Many organizations operate mixed environments with legacy systems that must integrate with modern applications. Long polling provides a bridge that works everywhere.
Legacy Integration Scenarios:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
// Minimal long polling client - works in IE11, old mobile browsers, anywhere (function(global) { 'use strict'; function SimpleLongPoll(options) { this.url = options.url; this.onMessage = options.onMessage || function() {}; this.onError = options.onError || function() {}; this.lastEventId = null; this.running = false; this.retryCount = 0; this.maxRetries = options.maxRetries || 10; } SimpleLongPoll.prototype.start = function() { this.running = true; this._poll(); }; SimpleLongPoll.prototype.stop = function() { this.running = false; if (this._xhr) { this._xhr.abort(); } }; SimpleLongPoll.prototype._poll = function() { if (!this.running) return; var self = this; var xhr = new XMLHttpRequest(); this._xhr = xhr; var url = this.url; if (this.lastEventId) { url += (url.indexOf('?') > -1 ? '&' : '?') + 'since=' + encodeURIComponent(this.lastEventId); } xhr.open('GET', url, true); xhr.setRequestHeader('Accept', 'application/json'); xhr.setRequestHeader('Cache-Control', 'no-cache'); xhr.timeout = 60000; // 60 second timeout xhr.onload = function() { if (xhr.status === 200) { self.retryCount = 0; // Reset on success try { var data = JSON.parse(xhr.responseText); if (data.events && data.events.length > 0) { // Update cursor var lastEvent = data.events[data.events.length - 1]; self.lastEventId = lastEvent.id || lastEvent.timestamp; // Deliver events for (var i = 0; i < data.events.length; i++) { self.onMessage(data.events[i]); } } } catch (e) { self.onError(e); } // Immediate reconnect self._poll(); } else if (xhr.status === 204) { // Timeout, no data - reconnect self._poll(); } else { // Error self._handleError(new Error('HTTP ' + xhr.status)); } }; xhr.onerror = function() { self._handleError(new Error('Network error')); }; xhr.ontimeout = function() { self._handleError(new Error('Request timeout')); }; xhr.send(); }; SimpleLongPoll.prototype._handleError = function(error) { this.onError(error); if (!this.running) return; this.retryCount++; if (this.retryCount > this.maxRetries) { this.onError(new Error('Max retries exceeded')); return; } // Exponential backoff var delay = Math.min(30000, Math.pow(2, this.retryCount) * 1000); var self = this; setTimeout(function() { self._poll(); }, delay); }; // Export for different module systems if (typeof module !== 'undefined' && module.exports) { module.exports = SimpleLongPoll; } else { global.SimpleLongPoll = SimpleLongPoll; } })(typeof window !== 'undefined' ? window : this); // Usage - works in any environmentvar poller = new SimpleLongPoll({ url: '/api/events/poll', onMessage: function(event) { console.log('Received:', event); }, onError: function(error) { console.error('Poll error:', error); }});poller.start();The implementation above uses only XMLHttpRequest—available in every browser since IE7 (2006). It requires no build tools, no polyfills, and no framework. This makes it ideal for embedded systems, kiosks, and any environment where you can't control the JavaScript runtime.
Mobile environments present unique challenges for real-time connections. Long polling offers advantages in specific mobile scenarios.
Mobile Network Characteristics:
| Scenario | WebSocket | Long Polling |
|---|---|---|
| WiFi to cellular switch | Connection drops, must reconnect | Next poll uses new network automatically |
| Carrier NAT timeout (60s) | Silent disconnect, delayed detection | Poll timeout < NAT timeout, graceful |
| App backgrounded | Connection killed by OS | No connection when not polling |
| Recovery after signal loss | Reconnect + resync logic needed | Cursor-based resume automatic |
| Battery impact | Constant keepalive messages | Network radio can sleep between polls |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
// Long polling optimized for mobile constraints class MobileLongPoll { private readonly config = { // Shorter timeout to avoid carrier NAT issues pollTimeout: 25000, // Longer timeout on mobile networks requestTimeout: 35000, // Minimal aggressive backoff minBackoff: 1000, maxBackoff: 30000, }; private isPolling = false; private appState: 'foreground' | 'background' = 'foreground'; constructor() { this.setupAppStateListeners(); this.setupNetworkListeners(); } private setupAppStateListeners() { document.addEventListener('visibilitychange', () => { if (document.hidden) { this.handleBackground(); } else { this.handleForeground(); } }); // React Native / Capacitor app state if (typeof AppState !== 'undefined') { AppState.addEventListener('change', (state) => { if (state === 'active') { this.handleForeground(); } else { this.handleBackground(); } }); } } private setupNetworkListeners() { // Detect network changes if ('connection' in navigator) { (navigator as any).connection.addEventListener('change', () => { this.handleNetworkChange(); }); } // Online/offline events window.addEventListener('online', () => this.handleOnline()); window.addEventListener('offline', () => this.handleOffline()); } private handleBackground() { this.appState = 'background'; // Stop polling to save battery this.stopPolling(); // Optionally: one final poll with short timeout to get latest state this.doSinglePoll(5000); } private handleForeground() { this.appState = 'foreground'; // Resume polling this.startPolling(); } private handleNetworkChange() { // Network changed - current poll may fail // Let it fail naturally, next poll will use new network console.log('Network changed, continuing with next poll'); // Optionally: abort current poll and start fresh // this.abortAndRestart(); } private handleOnline() { if (this.appState === 'foreground') { // Immediately poll to catch up on missed events this.poll(); } } private handleOffline() { // Don't try to poll when offline // Will resume automatically via handleOnline } private async poll() { if (!this.isPolling || this.appState === 'background') { return; } try { const response = await fetch('/api/poll', { signal: AbortSignal.timeout(this.config.requestTimeout), headers: { 'X-Long-Poll-Timeout': String(this.config.pollTimeout / 1000), }, }); // Process response... // Immediate next poll requestAnimationFrame(() => this.poll()); } catch (error) { // Handle error with backoff await this.handlePollError(error); } } private async doSinglePoll(timeout: number) { // Single poll without loop - for background updates try { const response = await fetch('/api/poll?timeout=' + timeout, { signal: AbortSignal.timeout(timeout + 5000), }); // Process final update before sleeping } catch { // Ignore - we're going to background anyway } }}On mobile, battery life is paramount. Long polling naturally allows the radio to sleep between polls (if timeout > 20s). WebSocket's persistent connection prevents this. For apps where real-time isn't critical when backgrounded, long polling with foreground-only polling is significantly more battery-friendly.
Major technology companies continue to use long polling for critical systems. These case studies illustrate when and why it remains the right choice.
Case Study 1: Gmail Notifications
Gmail used long polling as its primary notification mechanism for years (and may still use it as a fallback). Why?
Gmail's long polling implementation became the basis for Google's Channel API, enabling real-time features across Google services while maintaining universal compatibility.
Case Study 2: Slack's Transport Layer
Slack supports multiple real-time transports with long polling as a fallback:
123456789101112131415161718192021222324252627
// Slack's transport negotiation (simplified) class SlackConnection { async connect(): Promise<Transport> { // Try WebSocket first (fastest) try { const ws = await this.tryWebSocket(); console.log('Connected via WebSocket'); return ws; } catch (e) { console.log('WebSocket unavailable:', e.message); } // Fall back to long polling (always works) console.log('Falling back to long polling'); return new LongPollTransport(this.config); }} // Slack's statistics (approximate, from public sources):// - 70-80% of connections use WebSocket// - 20-30% use long polling (corporate networks, restrictive environments)// - Long polling users report identical user experience // Key insight: Slack could have required WebSocket and // forced enterprises to change networks. Instead, they // chose compatibility over forcing infrastructure changes.Case Study 3: Trello Real-Time Updates
Trello, the popular project management tool, uses long polling for board updates. Their reasoning:
Case Study 4: Financial Data Feeds
Surprisingly, some financial data feeds use long polling internally:
For example, a retail trading platform serving casual investors doesn't need HFT-grade latency. Long polling delivers "real-time enough" while ensuring every customer can receive updates regardless of their network.
You rarely hear about long polling successes because it's not sexy. Companies don't brag about using long polling—they brag about WebSockets. But the silent success of long polling across millions of enterprise deployments speaks to its enduring value.
We've completed our comprehensive exploration of long polling—from fundamental concepts through implementation details to practical decision-making. Let's consolidate the key insights:
Module Complete:
You've mastered long polling as a real-time communication pattern. You understand:
This knowledge enables you to make informed architecture decisions, defend those decisions to stakeholders, and implement reliable real-time systems that work for all your users.
Congratulations! You've completed the Long Polling module. You now have deep expertise in this often-underappreciated but critically important real-time communication pattern. You're equipped to implement, operate, and advocate for long polling when it's the right architectural choice.