Skip to main content

Express Long Polling

Introduction

In today's web applications, real-time communication between clients and servers has become increasingly important. Users expect instant updates without having to refresh their browsers. While WebSockets offer a robust solution for real-time communication, there are cases where a simpler approach like Long Polling is more appropriate.

Long polling is a technique that emulates real-time communication when WebSockets aren't available or are overkill for your application's needs. It's especially useful for beginners looking to implement basic real-time features without diving into more complex technologies.

In this tutorial, we'll explore how to implement long polling in Express.js applications. We'll cover:

  • What long polling is and how it differs from regular HTTP requests
  • Setting up an Express server for long polling
  • Implementing a basic long polling mechanism
  • Error handling and timeouts
  • Real-world applications of long polling

What is Long Polling?

Long polling is a technique where the client sends a request to the server, but the server doesn't respond immediately. Instead, the server holds the connection open until new data is available or a timeout occurs. Once the server responds, the client immediately sends another request, maintaining a continuous connection cycle.

Traditional Polling vs. Long Polling

Traditional Polling:

  1. Client makes a request for updates
  2. Server responds immediately, regardless of whether there's new data
  3. Client waits a set interval (e.g., 5 seconds)
  4. Client makes another request
  5. Repeat

Long Polling:

  1. Client makes a request for updates
  2. Server holds the connection open until:
    • New data is available, or
    • A timeout occurs
  3. Server responds when one of those conditions is met
  4. Client immediately makes a new request
  5. Repeat

Long polling reduces the number of unnecessary requests compared to traditional polling, making it more efficient.

Setting Up Your Express Server for Long Polling

Let's start by setting up a basic Express server that will support long polling:

javascript
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Serve static files
app.use(express.static('public'));
app.use(express.json());

// Store for our messages/events
const messages = [];
// Store for active connections
const connections = [];

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

Basic Long Polling Implementation

Now, let's implement the core long polling mechanism:

javascript
// Endpoint to add new messages
app.post('/api/messages', (req, res) => {
const message = {
id: Date.now(),
text: req.body.text,
timestamp: new Date()
};

messages.push(message);

// Notify all connected clients about the new message
connections.forEach(connection => {
connection.res.json([message]);
connection.res.end();
});

// Clear the connections array
connections.length = 0;

res.status(201).json(message);
});

// Long polling endpoint
app.get('/api/messages/poll', (req, res) => {
// Get the last message ID the client has
const lastId = parseInt(req.query.lastId) || 0;

// Check if there are new messages
const newMessages = messages.filter(msg => msg.id > lastId);

if (newMessages.length > 0) {
// If there are new messages, send them immediately
return res.json(newMessages);
}

// Otherwise, hold the connection open
const connection = {
res,
time: new Date()
};

connections.push(connection);

// Set a timeout to avoid keeping the connection open indefinitely
setTimeout(() => {
const index = connections.indexOf(connection);
if (index !== -1) {
connections.splice(index, 1);
res.json([]); // Return empty array if no updates
res.end();
}
}, 30000); // 30 seconds timeout
});

This implementation includes:

  1. A POST endpoint to add new messages
  2. A GET endpoint that implements long polling to fetch new messages
  3. A mechanism to track active connections
  4. A timeout to ensure connections don't stay open forever

Client-Side Implementation

Let's also implement the client-side code that will interact with our long polling server:

html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Express Long Polling Demo</title>
<style>
#message-container {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
margin-bottom: 10px;
padding: 10px;
}
</style>
</head>
<body>
<h1>Long Polling Chat</h1>
<div id="message-container"></div>
<form id="message-form">
<input type="text" id="message-input" placeholder="Type a message..." required>
<button type="submit">Send</button>
</form>

<script>
const messageForm = document.getElementById('message-form');
const messageInput = document.getElementById('message-input');
const messageContainer = document.getElementById('message-container');

let lastMessageId = 0;

// Function to send a new message
async function sendMessage(text) {
try {
const response = await fetch('/api/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});

if (!response.ok) {
throw new Error('Failed to send message');
}
} catch (error) {
console.error('Error sending message:', error);
}
}

// Function to poll for new messages
async function pollForMessages() {
try {
const response = await fetch(`/api/messages/poll?lastId=${lastMessageId}`);

if (!response.ok) {
throw new Error('Polling request failed');
}

const newMessages = await response.json();

if (newMessages.length > 0) {
// Update lastMessageId to the newest message
lastMessageId = newMessages[newMessages.length - 1].id;

// Display the new messages
newMessages.forEach(message => {
const messageElement = document.createElement('div');
messageElement.textContent = `${new Date(message.timestamp).toLocaleTimeString()}: ${message.text}`;
messageContainer.appendChild(messageElement);
});

// Scroll to the bottom
messageContainer.scrollTop = messageContainer.scrollHeight;
}

// Immediately poll again
pollForMessages();
} catch (error) {
console.error('Polling error:', error);
// If there was an error, wait before reconnecting
setTimeout(pollForMessages, 5000);
}
}

// Handle form submission
messageForm.addEventListener('submit', async (event) => {
event.preventDefault();
const text = messageInput.value.trim();
if (text) {
await sendMessage(text);
messageInput.value = '';
}
});

// Start polling when the page loads
document.addEventListener('DOMContentLoaded', () => {
pollForMessages();
});
</script>
</body>
</html>

Error Handling and Timeouts

When implementing long polling, proper error handling and timeouts are crucial:

javascript
// Improved long polling endpoint with better error handling
app.get('/api/messages/poll', (req, res) => {
// Set request timeout
req.setTimeout(35000, () => {
res.status(408).json({ error: 'Request timeout' });
});

// Handle client disconnect
req.on('close', () => {
const index = connections.findIndex(conn => conn.res === res);
if (index !== -1) {
connections.splice(index, 1);
}
});

const lastId = parseInt(req.query.lastId) || 0;
const newMessages = messages.filter(msg => msg.id > lastId);

if (newMessages.length > 0) {
return res.json(newMessages);
}

const connection = {
res,
time: new Date()
};

connections.push(connection);

// Set a timeout for the response
setTimeout(() => {
const index = connections.indexOf(connection);
if (index !== -1) {
connections.splice(index, 1);
res.json([]);
res.end();
}
}, 30000);
});

This enhanced version:

  1. Sets a request timeout
  2. Listens for client disconnects to clean up the connections array
  3. Still includes the 30-second timeout for long polling

Real-World Application: Notification System

Let's create a practical example: a notification system that uses long polling to alert users of new notifications.

javascript
// Server-side code
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.static('public'));
app.use(express.json());

// Store for notifications
const notifications = [];
// Store for active connections
const activeConnections = [];

// Admin endpoint to generate a notification
app.post('/api/admin/notifications', (req, res) => {
const notification = {
id: Date.now(),
title: req.body.title,
message: req.body.message,
type: req.body.type || 'info',
timestamp: new Date()
};

notifications.push(notification);

// Notify all connected clients
activeConnections.forEach(conn => {
conn.res.json([notification]);
conn.res.end();
});

// Clear connections since they've all been notified
activeConnections.length = 0;

res.status(201).json(notification);
});

// Long polling endpoint for notifications
app.get('/api/notifications/poll', (req, res) => {
const lastNotificationId = parseInt(req.query.lastId) || 0;
const newNotifications = notifications.filter(n => n.id > lastNotificationId);

if (newNotifications.length > 0) {
return res.json(newNotifications);
}

const connection = { res, time: new Date() };
activeConnections.push(connection);

// Clean up after 30 seconds
setTimeout(() => {
const index = activeConnections.indexOf(connection);
if (index !== -1) {
activeConnections.splice(index, 1);
res.json([]);
res.end();
}
}, 30000);
});

app.listen(PORT, () => {
console.log(`Notification server running on port ${PORT}`);
});

Client-side implementation for the notification system:

html
<!-- public/notifications.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notification System</title>
<style>
.notification {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.info { background-color: #d1ecf1; }
.success { background-color: #d4edda; }
.warning { background-color: #fff3cd; }
.error { background-color: #f8d7da; }
</style>
</head>
<body>
<h1>Real-time Notification System</h1>
<div id="notification-container"></div>

<script>
const notificationContainer = document.getElementById('notification-container');
let lastNotificationId = 0;

async function pollForNotifications() {
try {
const response = await fetch(`/api/notifications/poll?lastId=${lastNotificationId}`);

if (!response.ok) {
throw new Error('Polling request failed');
}

const newNotifications = await response.json();

if (newNotifications.length > 0) {
// Update the last ID
lastNotificationId = newNotifications[newNotifications.length - 1].id;

// Display notifications
newNotifications.forEach(notification => {
const notifElement = document.createElement('div');
notifElement.className = `notification ${notification.type}`;

const title = document.createElement('h3');
title.textContent = notification.title;

const message = document.createElement('p');
message.textContent = notification.message;

const time = document.createElement('small');
time.textContent = new Date(notification.timestamp).toLocaleString();

notifElement.appendChild(title);
notifElement.appendChild(message);
notifElement.appendChild(time);
notificationContainer.prepend(notifElement);

// Show browser notification if supported
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.message
});
}
});
}

// Poll again immediately
pollForNotifications();
} catch (error) {
console.error('Polling error:', error);
// Wait before trying again
setTimeout(pollForNotifications, 5000);
}
}

// Request notification permission
document.addEventListener('DOMContentLoaded', async () => {
if ('Notification' in window) {
if (Notification.permission !== 'granted' && Notification.permission !== 'denied') {
await Notification.requestPermission();
}
}

// Start polling
pollForNotifications();
});
</script>
</body>
</html>

Advantages and Disadvantages of Long Polling

Advantages

  1. Simple Implementation: Long polling is easier to implement than WebSockets.
  2. Wide Compatibility: Works in environments where WebSockets might be blocked.
  3. Less Overhead: For infrequent updates, long polling can be more efficient than keeping a WebSocket connection open.
  4. Firewall Friendly: Uses standard HTTP requests, which pass through most firewalls without issues.

Disadvantages

  1. Server Resources: Each long poll consumes a server connection until it completes.
  2. Scalability Issues: With many concurrent users, the number of open connections can strain server resources.
  3. Latency: There's still a slight delay between when an event happens and when clients receive it.
  4. Connection Management: Can be complex to manage timeouts and connection errors properly.

Best Practices

  1. Set Appropriate Timeouts: Don't keep connections open for too long. 30-60 seconds is typical.
  2. Implement Error Handling: Handle disconnects and reconnects gracefully on both server and client sides.
  3. Use Connection Pooling: If your server supports it, use connection pooling to handle many concurrent connections.
  4. Consider Alternatives: For high-traffic or truly real-time applications, WebSockets or Server-Sent Events may be better options.
  5. Implement Backoff Strategy: If connections fail, implement an exponential backoff strategy before reconnecting.

Summary

Long polling provides a way to achieve near real-time communication between client and server using standard HTTP requests. While it's not as efficient as WebSockets for truly real-time applications, it's simpler to implement and works in more environments.

In this tutorial, we've covered:

  • The basic concept of long polling and how it differs from traditional polling
  • Setting up an Express server for long polling
  • Implementing both server and client-side code
  • Error handling and timeouts
  • A real-world notification system example

Long polling is an important technique to understand in your journey of building interactive web applications. It provides a bridge between traditional request-response patterns and more advanced real-time communication methods.

Additional Resources

Exercises

  1. Implement a Timeout Dashboard: Create a dashboard that shows all current long-polling connections and their timeout status.
  2. Message Delivery Confirmation: Modify the chat example to confirm when messages are delivered to all connected clients.
  3. User Typing Indicator: Add a feature to the chat application that shows when a user is typing.
  4. Resilient Client: Enhance the client-side code to handle server disconnects gracefully with automatic reconnection.
  5. Server Load Testing: Write a script that simulates many clients connecting to your long polling server to test its limits.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)