Front-end Engineering Lab

Polling and Real-Time Sync

Keep data fresh with polling, WebSockets, and Server-Sent Events

Keep your app's data synchronized with the server. This guide covers polling and real-time update patterns.

🎯 When to Use Each

Polling: Simple, works everywhere, predictable load
WebSockets: Bi-directional, instant updates, complex
SSE: Server → Client only, simpler than WebSockets
Long Polling: Fallback for old browsers

⏱️ Pattern 1: Simple Polling

Fetch data at regular intervals.

import { useQuery } from '@tanstack/react-query';

function NotificationBell() {
  const { data: notifications } = useQuery({
    queryKey: ['notifications'],
    queryFn: fetchNotifications,
    refetchInterval: 30000, // Poll every 30 seconds
  });
  
  const unreadCount = notifications?.filter(n => !n.read).length || 0;
  
  return (
    <button>
      🔔 {unreadCount > 0 && <span>{unreadCount}</span>}
    </button>
  );
}

With Conditional Polling

function OrderStatus({ orderId }: { orderId: string }) {
  const { data: order } = useQuery({
    queryKey: ['order', orderId],
    queryFn: () => fetchOrder(orderId),
    refetchInterval: (data) => {
      // Stop polling when order is complete
      if (data?.status === 'delivered') {
        return false;
      }
      
      // Poll more frequently for pending orders
      if (data?.status === 'pending') {
        return 5000; // 5 seconds
      }
      
      // Poll less frequently otherwise
      return 30000; // 30 seconds
    },
  });
  
  return (
    <div>
      <h3>Order Status: {order?.status}</h3>
      <ProgressBar status={order?.status} />
    </div>
  );
}

Pros:

  • ✅ Simple to implement
  • ✅ Works everywhere
  • ✅ Predictable server load

Cons:

  • ⚠️ Wastes bandwidth (polling when no changes)
  • ⚠️ Delayed updates (up to interval time)
  • ⚠️ Battery drain on mobile

🚀 Pattern 2: Smart Polling (Adaptive Interval)

Adjust polling frequency based on activity.

class AdaptivePoller {
  private interval: number = 30000; // Start with 30s
  private minInterval: number = 5000; // Min 5s
  private maxInterval: number = 60000; // Max 60s
  private consecutiveNoChanges: number = 0;
  
  getInterval(): number {
    return this.interval;
  }
  
  onDataChanged() {
    // Data changed, poll more frequently
    this.interval = Math.max(this.minInterval, this.interval / 2);
    this.consecutiveNoChanges = 0;
  }
  
  onDataUnchanged() {
    // No changes, slow down polling
    this.consecutiveNoChanges++;
    
    if (this.consecutiveNoChanges >= 3) {
      this.interval = Math.min(this.maxInterval, this.interval * 1.5);
    }
  }
  
  reset() {
    this.interval = 30000;
    this.consecutiveNoChanges = 0;
  }
}

// Usage
function useAdaptivePolling(queryKey: string[], queryFn: () => Promise<any>) {
  const poller = useRef(new AdaptivePoller());
  const [prevData, setPrevData] = useState<any>(null);
  
  const { data } = useQuery({
    queryKey,
    queryFn,
    refetchInterval: poller.current.getInterval(),
  });
  
  useEffect(() => {
    if (data) {
      if (JSON.stringify(data) !== JSON.stringify(prevData)) {
        poller.current.onDataChanged();
      } else {
        poller.current.onDataUnchanged();
      }
      setPrevData(data);
    }
  }, [data, prevData]);
  
  return data;
}

// Example usage
function NotificationBell() {
  const notifications = useAdaptivePolling(
    ['notifications'],
    fetchNotifications
  );
  
  return <div>🔔 {notifications?.length}</div>;
}

🌐 Pattern 3: WebSocket Real-Time Updates

Bi-directional, instant updates.

// WebSocket hook
function useWebSocket(url: string) {
  const [socket, setSocket] = useState<WebSocket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  
  useEffect(() => {
    const ws = new WebSocket(url);
    
    ws.onopen = () => {
      console.log('WebSocket connected');
      setIsConnected(true);
    };
    
    ws.onclose = () => {
      console.log('WebSocket disconnected');
      setIsConnected(false);
      
      // Reconnect after 5 seconds
      setTimeout(() => {
        setSocket(new WebSocket(url));
      }, 5000);
    };
    
    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };
    
    setSocket(ws);
    
    return () => {
      ws.close();
    };
  }, [url]);
  
  return { socket, isConnected };
}

// Real-time notifications
function NotificationBell() {
  const { socket, isConnected } = useWebSocket('wss://api.example.com/notifications');
  const queryClient = useQueryClient();
  
  useEffect(() => {
    if (!socket) return;
    
    socket.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      
      // Update query cache
      queryClient.setQueryData<Notification[]>(['notifications'], (old = []) => [
        notification,
        ...old
      ]);
      
      // Show toast
      toast.info(notification.message);
    };
  }, [socket, queryClient]);
  
  const { data: notifications = [] } = useQuery({
    queryKey: ['notifications'],
    queryFn: fetchNotifications
  });
  
  return (
    <div>
      🔔 {notifications.length}
      {!isConnected && <span>⚠️ Reconnecting...</span>}
    </div>
  );
}

With Reconnection Logic

class WebSocketClient {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelay = 1000;
  
  constructor(url: string) {
    this.url = url;
    this.connect();
  }
  
  private connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      console.log('Connected');
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000;
    };
    
    this.ws.onclose = () => {
      console.log('Disconnected');
      this.reconnect();
    };
    
    this.ws.onerror = (error) => {
      console.error('Error:', error);
    };
  }
  
  private reconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnect attempts reached');
      return;
    }
    
    this.reconnectAttempts++;
    
    console.log(
      `Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`
    );
    
    setTimeout(() => {
      this.connect();
      this.reconnectDelay *= 2; // Exponential backoff
    }, this.reconnectDelay);
  }
  
  send(data: any) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
    }
  }
  
  onMessage(callback: (data: any) => void) {
    if (this.ws) {
      this.ws.onmessage = (event) => {
        callback(JSON.parse(event.data));
      };
    }
  }
  
  close() {
    this.ws?.close();
  }
}

Pros:

  • ✅ Instant updates (< 100ms)
  • ✅ Bi-directional communication
  • ✅ Efficient for frequent updates

Cons:

  • ⚠️ More complex setup
  • ⚠️ Requires WebSocket server
  • ⚠️ Connection management needed

📡 Pattern 4: Server-Sent Events (SSE)

Server → Client only, simpler than WebSockets.

function useServerSentEvents(url: string) {
  const [data, setData] = useState<any>(null);
  const [isConnected, setIsConnected] = useState(false);
  
  useEffect(() => {
    const eventSource = new EventSource(url);
    
    eventSource.onopen = () => {
      console.log('SSE connected');
      setIsConnected(true);
    };
    
    eventSource.onmessage = (event) => {
      const newData = JSON.parse(event.data);
      setData(newData);
    };
    
    eventSource.onerror = () => {
      console.error('SSE error');
      setIsConnected(false);
      eventSource.close();
      
      // Reconnect after 5 seconds
      setTimeout(() => {
        // EventSource will auto-reconnect
      }, 5000);
    };
    
    return () => {
      eventSource.close();
    };
  }, [url]);
  
  return { data, isConnected };
}

// Usage - Live order updates
function OrderTracking({ orderId }: { orderId: string }) {
  const { data: orderUpdate, isConnected } = useServerSentEvents(
    `/api/orders/${orderId}/updates`
  );
  
  return (
    <div>
      <h3>Order Status</h3>
      {!isConnected && <span>Reconnecting...</span>}
      {orderUpdate && (
        <div>
          <p>Status: {orderUpdate.status}</p>
          <p>Location: {orderUpdate.location}</p>
          <p>ETA: {orderUpdate.eta}</p>
        </div>
      )}
    </div>
  );
}

Backend (Node.js)

app.get('/api/orders/:id/updates', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });
  
  // Send update every 5 seconds
  const interval = setInterval(() => {
    const update = getOrderUpdate(req.params.id);
    res.write(`data: ${JSON.stringify(update)}\n\n`);
  }, 5000);
  
  // Cleanup on disconnect
  req.on('close', () => {
    clearInterval(interval);
    res.end();
  });
});

Pros:

  • ✅ Simpler than WebSockets
  • ✅ Auto-reconnect built-in
  • ✅ Works with HTTP/2

Cons:

  • ⚠️ Server → Client only
  • ⚠️ Less browser support than WebSockets

🔄 Pattern 5: Long Polling

Fallback for old browsers.

async function longPoll(url: string, callback: (data: any) => void) {
  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' }
    });
    
    if (response.ok) {
      const data = await response.json();
      callback(data);
    }
    
    // Immediately start next long poll
    longPoll(url, callback);
  } catch (error) {
    console.error('Long poll error:', error);
    
    // Retry after delay
    setTimeout(() => {
      longPoll(url, callback);
    }, 5000);
  }
}

// Usage
function NotificationBell() {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  
  useEffect(() => {
    longPoll('/api/notifications/poll', (newNotification) => {
      setNotifications(prev => [newNotification, ...prev]);
    });
  }, []);
  
  return <div>🔔 {notifications.length}</div>;
}

Backend (Node.js)

app.get('/api/notifications/poll', async (req, res) => {
  const userId = req.user.id;
  
  // Wait for new notification (max 30 seconds)
  const timeout = setTimeout(() => {
    res.json({ type: 'timeout' });
  }, 30000);
  
  // Listen for new notification
  const listener = (notification) => {
    clearTimeout(timeout);
    res.json(notification);
  };
  
  notificationEmitter.once(`notification:${userId}`, listener);
  
  // Cleanup on disconnect
  req.on('close', () => {
    clearTimeout(timeout);
    notificationEmitter.off(`notification:${userId}`, listener);
  });
});

🎯 Pattern 6: Hybrid Approach

Combine polling with WebSockets.

function useRealtimeData<T>(
  queryKey: string[],
  queryFn: () => Promise<T>,
  websocketUrl?: string
) {
  const queryClient = useQueryClient();
  
  // Initial fetch + polling fallback
  const { data } = useQuery({
    queryKey,
    queryFn,
    refetchInterval: websocketUrl ? false : 30000, // Poll only if no WS
  });
  
  // WebSocket for real-time updates
  useEffect(() => {
    if (!websocketUrl) return;
    
    const ws = new WebSocket(websocketUrl);
    
    ws.onmessage = (event) => {
      const update = JSON.parse(event.data);
      
      // Update cache with real-time data
      queryClient.setQueryData<T>(queryKey, (old) => ({
        ...old,
        ...update
      }));
    };
    
    ws.onerror = () => {
      // Fallback to polling on WebSocket error
      queryClient.invalidateQueries({ queryKey });
    };
    
    return () => ws.close();
  }, [websocketUrl, queryKey, queryClient]);
  
  return data;
}

// Usage
function Dashboard() {
  const stats = useRealtimeData(
    ['stats'],
    fetchStats,
    'wss://api.example.com/stats'
  );
  
  return <div>Users online: {stats?.usersOnline}</div>;
}

📊 Comparison Table

MethodLatencyComplexityServer LoadUse Case
Simple Polling5-30sHighNotifications
Adaptive Polling5-60s⭐⭐MediumDynamic data
WebSockets< 100ms⭐⭐⭐⭐LowChat, gaming
SSE< 100ms⭐⭐LowLive feeds
Long Polling< 1s⭐⭐⭐HighFallback
Hybrid< 100ms⭐⭐⭐⭐LowProduction apps

🏢 Real-World Examples

Slack

// WebSockets for messages
// Polling fallback
// Reconnection logic

Gmail

// Long polling for new emails
// SSE for real-time presence
// Smart polling when idle

Twitter

// WebSockets for timeline updates
// Polling for notifications
// Adaptive intervals

📚 Key Takeaways

  1. Start with polling - Simple and works
  2. Use WebSockets for real-time (< 1s updates)
  3. SSE for server → client only
  4. Adaptive polling to reduce waste
  5. Always have reconnection logic
  6. Hybrid approach for production
  7. Monitor battery impact on mobile

Choose based on latency requirements and complexity tolerance.

On this page