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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)