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:
- Client makes a request for updates
- Server responds immediately, regardless of whether there's new data
- Client waits a set interval (e.g., 5 seconds)
- Client makes another request
- Repeat
Long Polling:
- Client makes a request for updates
- Server holds the connection open until:
- New data is available, or
- A timeout occurs
- Server responds when one of those conditions is met
- Client immediately makes a new request
- 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:
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:
// 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:
- A
POST
endpoint to add new messages - A
GET
endpoint that implements long polling to fetch new messages - A mechanism to track active connections
- 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:
<!-- 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:
// 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:
- Sets a request timeout
- Listens for client disconnects to clean up the connections array
- 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.
// 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:
<!-- 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
- Simple Implementation: Long polling is easier to implement than WebSockets.
- Wide Compatibility: Works in environments where WebSockets might be blocked.
- Less Overhead: For infrequent updates, long polling can be more efficient than keeping a WebSocket connection open.
- Firewall Friendly: Uses standard HTTP requests, which pass through most firewalls without issues.
Disadvantages
- Server Resources: Each long poll consumes a server connection until it completes.
- Scalability Issues: With many concurrent users, the number of open connections can strain server resources.
- Latency: There's still a slight delay between when an event happens and when clients receive it.
- Connection Management: Can be complex to manage timeouts and connection errors properly.
Best Practices
- Set Appropriate Timeouts: Don't keep connections open for too long. 30-60 seconds is typical.
- Implement Error Handling: Handle disconnects and reconnects gracefully on both server and client sides.
- Use Connection Pooling: If your server supports it, use connection pooling to handle many concurrent connections.
- Consider Alternatives: For high-traffic or truly real-time applications, WebSockets or Server-Sent Events may be better options.
- 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
- Mozilla Developer Network (MDN) - Comet Programming
- Express.js Documentation
- Socket.IO - A library that uses WebSockets with long polling fallback
- Server-Sent Events (SSE) - Another alternative to consider
Exercises
- Implement a Timeout Dashboard: Create a dashboard that shows all current long-polling connections and their timeout status.
- Message Delivery Confirmation: Modify the chat example to confirm when messages are delivered to all connected clients.
- User Typing Indicator: Add a feature to the chat application that shows when a user is typing.
- Resilient Client: Enhance the client-side code to handle server disconnects gracefully with automatic reconnection.
- 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! :)