Introduction: Why Do You Need Task Queues in JavaScript?
\nIn modern web development with Node.js, tasks often arise that cannot be completed instantly within a single HTTP request. Sending email newsletters, generating PDF reports, processing images, synchronizing with external APIs — all these operations require time and resources. If you execute them synchronously, the user will wait too long for a response, and the server may become overloaded.
\nJob Queues solve this problem. They allow you to defer heavy operations, distribute the load, guarantee reprocessing in case of failures, and scale the system horizontally. In this article, we will cover the practical aspects of working with queues in JavaScript (Node.js): from theory to writing working code using popular libraries.
\n\nBasic Concepts of Task Queues
\nBefore moving to the code, it is important to understand the key elements of any task queue:
\n- \n
- Job — a unit of work that needs to be performed. It usually contains data (payload) and meta-information (priority, attempts, status). \n
- Queue — a data structure (often based on Redis) that stores tasks in the order they arrive. \n
- Worker — a process or thread that picks up tasks from the queue and executes them. \n
- Producer — the part of the system that adds tasks to the queue. \n
- Consumer — a synonym for a worker that processes tasks. \n
In Node.js, the most popular libraries for working with queues are Bull and Kue. Both use Redis as a message broker. Bull is more modern and performant, while Kue is simpler to start with. In this article, we will focus on Bull as the recommended solution for production.
\n\nPractical Implementation of a Queue Using Bull
\nInstallation and Setup
\nFirst, install Redis (locally or via Docker) and add the library to your project:
\nnpm install bull\nBasic queue initialization looks like this:
\nconst Queue = require('bull');\nconst imageQueue = new Queue('image processing', {\n redis: { host: 'localhost', port: 6379 }\n});\n\nAdding Tasks (Producer)
\nThe producer adds a task with data and options:
\napp.post('/upload', async (req, res) => {\n const job = await imageQueue.add({\n imageUrl: req.body.url,\n userId: req.user.id\n }, {\n attempts: 3, // number of retries on error\n backoff: 5000, // delay between retries (ms)\n removeOnComplete: true // remove the task after success\n });\n res.json({ jobId: job.id });\n});\n\nProcessing Tasks (Worker)
\nThe worker is a separate process (or the same process, but asynchronous) that listens to the queue:
\nimageQueue.process(async (job, done) => {\n try {\n const { imageUrl, userId } = job.data;\n console.log(`Processing image ${imageUrl} for user ${userId}`);\n // Your logic here: download, resize, save\n await processImage(imageUrl);\n done(); // signal successful completion\n } catch (error) {\n done(new Error('Processing error: ' + error.message));\n }\n});\n\nError Handling and Retries
\nBull automatically retries a task if the worker completes with an error. You can configure the retry strategy:
\nconst job = await imageQueue.add(data, {\n attempts: 5,\n backoff: { type: 'exponential', delay: 2000 }\n});\nYou can also listen to queue events for monitoring:
\nimageQueue.on('completed', (job, result) => {\n console.log(`Task ${job.id} completed`);\n});\nimageQueue.on('failed', (job, err) => {\n console.error(`Task ${job.id} failed: ${err.mes