# BullMQ with Node.js and Redis

Imagine you're building an e-commerce application. Whenever a customer places an order, your application needs to:

*   Save the order in the database.
    
*   Send an order confirmation email.
    
*   Generate an invoice.
    
*   Notify the warehouse.
    
*   Send an SMS to the customer.
    

Should your server perform all these tasks before responding to the customer?

Not necessarily. Some tasks take time, and others depend on external services that might be slow or temporarily unavailable.

If your server waits for everything to finish, the customer may experience a slow response. If an external email service fails, it could also interrupt the overall process.

**A queue solves this problem by allowing your application to schedule work for processing separately from the request.**

Instead of doing everything immediately, your server can save the order, add background tasks to a queue, and respond to the customer. Another part of your application processes those tasks in the background.

This is where **Redis and BullMQ** come in.

## What Is a Queue?

A queue is a way to store tasks that need to be processed.

Think of a queue at a bank. Customers arrive, wait for their turn, and are served in an organized manner.

A software queue follows a similar idea:

1.  A producer adds a job to the queue.
    
2.  The queue holds the job until it can be processed.
    
3.  A worker picks up the job.
    
4.  The worker performs the required task.
    
5.  The job is marked as completed or failed.
    

For example:

```text
Customer places an order
          |
          v
     Node.js API
          |
          v
   Add job to queue
          |
          v
      Redis
          |
          v
   BullMQ Worker
          |
          v
   Send confirmation email
```

The important idea is that **the API and the background worker do not need to perform their work at the same time or in the same process.**

### What are Producer, Queue, and Worker?

| Component | Meaning | Example |
| --- | --- | --- |
| Producer | Creates and submits jobs | Order API |
| Queue | Organizes jobs waiting for processing | Email queue |
| Worker | Executes jobs | Email sender |
| Redis | Stores the queue's data and state | Redis server |
| Job | One unit of work | Send an email to a customer |

These are the basic concepts you should understand before writing any BullMQ code.

## What Is Redis, and Why Do We Need It?

Redis is an in-memory data store that can store and retrieve data very quickly. It supports data structures such as strings, lists, hashes, and sorted sets.

BullMQ uses Redis to manage its jobs and their state.

For example, Redis helps BullMQ keep track of jobs that are:

*   Waiting to be processed.
    
*   Currently being processed.
    
*   Completed successfully.
    
*   Failed and potentially eligible for retry.
    

You do not need to implement these queue operations yourself. BullMQ manages them using Redis.

### Why not store the queue in a normal JavaScript array?

Consider this code:

```javascript
const jobs = [];

jobs.push({ type: "send-email", email: "user@example.com" });
```

This works as a basic demonstration, but it has several limitations:

*   The jobs exist only in that application's memory.
    
*   Restarting the process can lose the jobs.
    
*   Other Node.js processes cannot automatically share the same array.
    
*   You must implement job tracking, retries, and coordination yourself.
    

Redis gives multiple application processes access to shared queue data, while BullMQ handles queue management.

**Important:** Redis is not the same thing as BullMQ. Redis stores and coordinates the data; BullMQ provides the job-queue functionality.

## What Is BullMQ?

BullMQ is a Node.js library for managing background jobs using Redis.

It provides features such as:

*   Adding jobs to queues.
    
*   Processing jobs in the background.
    
*   Retrying failed jobs.
    
*   Limiting how frequently jobs are processed.
    
*   Scheduling delayed jobs.
    
*   Running multiple workers.
    
*   Tracking job status and results.
    

For example, instead of sending an email inside your API route, you can add an email job to BullMQ. A separate worker can process that job.

### How do Redis and BullMQ work together?

```text
                 Node.js Application
                         |
                         v
                    BullMQ Queue
                         |
                         v
                       Redis
                         |
                         v
                    BullMQ Worker
                         |
                         v
                   Actual Job Logic
```

The producer and worker can run in separate Node.js processes. They do not need to share JavaScript memory because they communicate through the queue backed by Redis.

## How the Complete Flow Works

Let's use the example of sending a welcome email after a user registers.

**Step 1: A user registers.**

The API creates the user's account.

**Step 2: The producer creates a job.**

The application adds a job such as `send-welcome-email` to the email queue.

**Step 3: BullMQ stores the job's queue state in Redis.**

The job waits until a worker is ready to process it.

**Step 4: The worker picks up the job.**

BullMQ gives the worker the job's data, including the user's email address.

**Step 5: The worker executes the task.**

The worker calls an email service and sends the welcome email.

**Step 6: BullMQ updates the job's status.**

If the task succeeds, the job becomes completed. If it fails, BullMQ can retry it according to the configured retry policy.

The user does not have to wait for the email to be sent before the registration API responds.

## Let's Build a Small BullMQ Project

Now let's implement everything from scratch using Node.js, BullMQ, and Redis running inside a Docker container.

We'll build a small **background email notification system**. To keep the example beginner-friendly, we'll print the email details in the terminal instead of connecting to a real email provider.

### What will we build?

*   A Redis server running inside Docker.
    
*   A Node.js producer that adds email jobs.
    
*   A BullMQ worker that processes those jobs.
    
*   Automatic retries when a job fails.
    

### Step 1: Create the project

Create a new folder and initialize Node.js.

```bash
mkdir bullmq-demo
cd bullmq-demo
npm init -y
```

Install the required packages:

```bash
npm install bullmq express dotenv
```

Here is what each package does:

| Package | Purpose |
| --- | --- |
| `bullmq` | Creates queues and processes jobs |
| `express` | Creates a simple HTTP API |
| `dotenv` | Loads configuration from a `.env` file |

BullMQ uses `ioredis` internally for Redis connections, so you don't need to install it separately for this example. [https://docs.bullmq.io/guide/connections](https://docs.bullmq.io/guide/connections)

### Step 2: Start Redis using Docker

Create a file named `docker-compose.yml` in your project root.

```yaml
services:
  redis:
    image: redis:7-alpine
    container_name: bullmq-redis
    ports:
      - "6379:6379"
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  redis_data:
```

Let's understand the important parts:

*   `image`: Downloads and runs Redis using Docker.
    
*   `ports`: Makes Redis available on port `6379` on your computer.
    
*   `appendonly`: Enables Redis's append-only persistence mechanism.
    
*   `volumes`: Keeps Redis data in a Docker-managed volume across container recreation.
    
*   `restart`: Restarts the container automatically under the specified conditions.
    

Start Redis:

```bash
docker compose up -d
```

Check whether the container is running:

```bash
docker ps
```

Test the Redis connection:

```bash
docker exec -it bullmq-redis redis-cli ping
```

Expected output:

```text
PONG
```

Your Redis server is now running inside Docker.

**Important:** This configuration is suitable for local learning, not a complete production setup. It exposes Redis without authentication, so do not expose this configuration to an untrusted network. Production deployments also need appropriate access controls, persistence policies, monitoring, and backups.

### Step 3: Configure the project

Create a `.env` file:

```env
PORT=3000
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
```

Our Node.js application runs on your computer, while Redis runs inside Docker. That's why we connect to `127.0.0.1:6379`.

If Node.js also ran inside Docker Compose, the connection would instead use the Redis service name:

```env
REDIS_HOST=redis
REDIS_PORT=6379
```

Inside a Docker Compose network, `redis` resolves to the Redis container. The name `localhost` inside your Node.js container would refer to that Node.js container itself, not the Redis container.

### Step 4: Create the queue

Create a file named `queue.js`.

```javascript
const { Queue } = require("bullmq");
require("dotenv").config();

const emailQueue = new Queue("email-queue", {
  connection: {
    host: process.env.REDIS_HOST,
    port: Number(process.env.REDIS_PORT),
  },
});

module.exports = emailQueue;
```

Let's break this down.

`new Queue("email-queue")`

Creates a BullMQ queue named `email-queue`. Jobs added to this queue can be processed by workers using the same queue name and Redis instance.

`connection`

Tells BullMQ where Redis is running.

`module.exports`

Allows other files to import and use this queue.

Think of `queue.js` as the shared entry point for submitting email jobs. The worker will connect to the same Redis server and listen to the same queue name.

### Step 5: Create the producer API

Create `server.js`.

```javascript
const express = require("express");
const emailQueue = require("./queue");
require("dotenv").config();

const app = express();

app.use(express.json());

app.post("/send-email", async (req, res) => {
  try {
    const { email, subject, message } = req.body;

    if (!email || !subject || !message) {
      return res.status(400).json({
        error: "Email, subject, and message are required",
      });
    }

    const job = await emailQueue.add(
      "send-email",
      { email, subject, message },
      {
        attempts: 3,
        backoff: {
          type: "exponential",
          delay: 1000,
        },
        removeOnComplete: 100,
        removeOnFail: 500,
      }
    );

    return res.status(202).json({
      message: "Email job added to queue",
      jobId: job.id,
    });
  } catch (error) {
    console.error("Failed to add job:", error);

    return res.status(500).json({
      error: "Could not add email job",
    });
  }
});

const server = app.listen(process.env.PORT || 3000, () => {
  console.log("API running on port", process.env.PORT || 3000);
});

async function shutdown() {
  server.close();
  await emailQueue.close();
  process.exit(0);
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
```

Here's the most important part:

```javascript
const job = await emailQueue.add(
  "send-email",
  { email, subject, message },
  {
    attempts: 3,
    backoff: {
      type: "exponential",
      delay: 1000,
    },
  }
);
```

The first argument is the **job name**. The second contains the job's data. The third defines job options.

In this example:

*   `attempts: 3` allows up to three processing attempts in total.
    
*   `backoff` introduces increasing delays between retries.
    
*   `removeOnComplete: 100` retains a limited number of completed jobs.
    
*   `removeOnFail: 500` retains a limited number of failed jobs.
    

A failed job is not automatically retried forever. Its retry behavior depends on the configured attempts and backoff policy.

The API returns HTTP `202 Accepted`, meaning the job was accepted for background processing. It does not mean that the email has already been sent.

### Step 6: Create the worker

Create a file named `worker.js`.

```javascript
const { Worker } = require("bullmq");
require("dotenv").config();

const worker = new Worker(
  "email-queue",
  async (job) => {
    const { email, subject, message } = job.data;

    console.log("Processing job:", job.id);
    console.log("Sending email to:", email);
    console.log("Subject:", subject);
    console.log("Message:", message);

    // Simulate a slow background task.
    await new Promise((resolve) => setTimeout(resolve, 2000));

    console.log("Email processed successfully!");

    return {
      success: true,
      email,
    };
  },
  {
    connection: {
      host: process.env.REDIS_HOST,
      port: Number(process.env.REDIS_PORT),
    },
    concurrency: 5,
  }
);

worker.on("completed", (job, result) => {
  console.log("Job completed:", job.id, result);
});

worker.on("failed", (job, error) => {
  console.error("Job failed:", job?.id, error.message);
});

worker.on("error", (error) => {
  console.error("Worker error:", error);
});

async function shutdown() {
  await worker.close();
  process.exit(0);
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

console.log("Email worker is running...");
```

### Understanding the worker

`new Worker("email-queue", ...)`

Tells BullMQ to process jobs from the `email-queue` queue.

`job.data`

Contains the information provided by the producer.

`concurrency: 5`

Allows this worker to process up to five jobs concurrently when they are available and the processor permits it. Actual throughput depends on the work being performed and available resources.

`worker.on("completed", ...)`

Runs when a job completes successfully.

`worker.on("failed", ...)`

Reports job failures. A job may be retried, so a failure event does not necessarily mean that all attempts have been exhausted.

The worker is where you would eventually integrate a real email provider, invoice generator, image processor, or other background task.

### Step 7: Run the application

Open two terminal windows in the project directory.

Terminal 1 — start the worker:

```bash
node worker.js
```

Expected output:

```text
Email worker is running...
```

Terminal 2 — start the API:

```bash
node server.js
```

Expected output:

```text
API running on port 3000
```

Now send a test request using `curl`:

```bash
curl -X POST http://localhost:3000/send-email \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "subject": "Welcome!",
    "message": "Thanks for signing up."
  }'
```

Example response:

```json
{
  "message": "Email job added to queue",
  "jobId": "1"
}
```

The actual job ID is generated by BullMQ and may differ.

Check the worker terminal. After a short delay, you'll see the job being processed.

```text
Processing job: 1
Sending email to: user@example.com
Subject: Welcome!
Message: Thanks for signing up.
Email processed successfully!
Job completed: 1 { success: true, email: 'user@example.com' }
```

You have now implemented a working background-job system.

### Step 8: Test retries

To see retries in action, temporarily add this line inside the worker processor:

```javascript
throw new Error("Simulated email failure");
```

Place it before the successful completion log.

Restart the worker, then submit another request. BullMQ will attempt the job again according to the retry configuration.

Because the job is configured for three attempts, it can be processed up to three times before being marked as permanently failed for that retry cycle.

Remove the test error after you've verified the behavior.

## What Happens When the API and Worker Run on Different Servers?

This is one of the most important concepts for interviews and real-world backend development.

In our example, the API and worker run on the same computer but in separate processes. In production, they can run on completely different servers.

Imagine this architecture:

```text
             Client / Frontend
                     |
                     v
              Load Balancer
                     |
                     v
               API Servers
             (Producers)
                     |
                     v
              Redis Server
              (Queue Data)
                     |
          +----------+----------+
          |                     |
          v                     v
      Worker 1              Worker 2
          |                     |
          v                     v
     Email Service        Invoice Service
```

Let's understand how communication works.

1.  The client sends an HTTP request to an API server.
    
2.  The API server creates a job and sends it to Redis through BullMQ.
    
3.  Redis stores the job and its queue state.
    
4.  A worker connected to the same Redis instance receives the job.
    
5.  The worker performs the task and updates the job's state through BullMQ.
    

**The API server does not need to know the worker's IP address.** It only needs to connect to the configured Redis endpoint and submit a job to the correct queue.

Likewise, the worker needs the Redis endpoint and the queue name to find the jobs it should process.

### What if the API and worker use different technologies?

The general messaging idea is the same, but compatibility matters.

For example, two Node.js services can use BullMQ directly. If another service is written in Python, it cannot simply import the Node.js BullMQ package. It needs a compatible integration or a separate messaging mechanism.

When building a system with multiple programming languages, choose a queue or messaging technology with suitable clients and protocols for every participating service.

### How does this work in microservices?

Suppose an e-commerce platform has these services:

| Service | Responsibility |
| --- | --- |
| Order Service | Creates orders |
| Payment Service | Processes payments |
| Notification Service | Sends emails and SMS |
| Inventory Service | Updates stock |
| Invoice Service | Generates invoices |

Instead of making the Order Service wait for every service to finish, it can publish jobs for suitable background tasks.

For example:

```text
Order Service
     |
     v
 Add notification job
     |
     v
   Redis
     |
     v
Notification Worker
     |
     v
 Send confirmation email
```

This reduces direct dependencies between the services.

However, queues do not replace every form of communication. A normal HTTP request may still be more appropriate when the caller needs an immediate response, such as checking a product's current price.

Also, a queued job does not automatically guarantee that an entire business workflow succeeds. Payment, inventory, and order confirmation may need explicit workflow coordination, compensation logic, or other reliability mechanisms.

## Common Use Cases of BullMQ

BullMQ is especially useful for work that can happen after the main request has been handled.

| Use case | Example |
| --- | --- |
| Email notifications | Welcome emails, password resets |
| Image processing | Resizing uploaded images |
| Video processing | Generating thumbnails |
| Report generation | Creating PDF reports |
| Scheduled tasks | Sending reminders tomorrow |
| Data processing | Processing uploaded CSV files |
| Webhooks | Retrying delivery to external systems |
| AI applications | Running long-running document processing jobs |
| E-commerce | Generating invoices and sending order updates |
| Bulk operations | Importing thousands of records |

### When should you use BullMQ?

Use it when tasks are slow, can be processed asynchronously, need retries, or must be distributed across multiple workers.

You probably don't need it for every operation. If your API simply fetches a small database record and returns it immediately, adding a queue may introduce unnecessary complexity.

## Important Queue Concepts for Interviews

These are the concepts worth understanding beyond simply memorizing the code.

### What is concurrency?

Concurrency means allowing multiple jobs to be in progress at the same time.

For example, a worker configured with `concurrency: 5` can handle multiple jobs concurrently. This can improve throughput when tasks involve waiting for network requests or other asynchronous operations.

### What is horizontal scaling?

Horizontal scaling means adding more worker processes or machines rather than relying on one increasingly powerful server.

```text
                Redis Queue
               /     |     \
              v      v      v
          Worker 1 Worker 2 Worker 3
```

If the number of jobs increases, you can add more workers, provided that Redis, downstream services, and other resources can handle the increased load.

### What happens when a job fails?

A job can fail because of a network issue, an unavailable service, invalid input, or an error in your code.

BullMQ supports retries. You configure how many attempts are allowed and, optionally, how long to wait between attempts.

A retry does not fix a permanent problem. If an email address is invalid, repeating the same operation may not help.

### What is exponential backoff?

Exponential backoff increases the waiting time between retries.

For example, with an initial delay of one second, the delays may grow approximately like this:

```text
Attempt 1 fails
      |
      v
Wait 1 second
      |
      v
Attempt 2 fails
      |
      v
Wait 2 seconds
      |
      v
Attempt 3 fails
      |
      v
Job remains failed after the allowed attempts
```

The precise retry behavior depends on the configuration and attempt numbering. The purpose is to avoid repeatedly hitting an unhealthy service too quickly.

### What if a worker crashes?

BullMQ uses job locks and recovery mechanisms to detect jobs that are no longer being processed properly. A stalled job can be made available for processing again.

However, this introduces an important lesson: **a job can sometimes execute more than once.**

Imagine a worker sends an email successfully but crashes before BullMQ records the job as completed. The job may be processed again.

For this reason, do not assume every job executes exactly once in all failure scenarios.

### What is idempotency?

Idempotency means that repeating an operation does not produce an unwanted additional effect.

For example, a payment worker should not charge the customer twice if a job is retried.

You can implement this by using a unique payment reference, enforcing database constraints, and checking whether the payment has already been processed before charging again.

For email notifications, you might store a unique notification ID and track whether the notification has already been sent.

Idempotency is one of the most important reliability concepts to understand when working with background jobs.

### What is the difference between a queue and a worker?

A queue accepts and manages jobs. A worker executes them.

A queue does not mean that the task has been completed. Adding a job successfully means it has been accepted by the queue, not that the underlying business operation has succeeded.

### Does BullMQ guarantee that no jobs are ever lost?

No system should be assumed to provide that guarantee under every possible failure.

BullMQ manages job state and supports recovery, but the reliability of the complete system also depends on Redis persistence, infrastructure, application design, and how failures are handled.

For production systems, configure Redis persistence appropriately, use a reliable Redis deployment, and test failure recovery.

## How Would You Deploy BullMQ in Production?

The local project is useful for learning, but a production deployment usually separates the API, worker, and Redis infrastructure.

### Recommended deployment structure

```text
                 Internet
                    |
                    v
             Load Balancer
                    |
             +------+------+
             |             |
             v             v
          API 1          API 2
             |             |
             +------+------+
                    |
                    v
             Managed Redis
                    |
             +------+------+
             |             |
             v             v
          Worker 1      Worker 2
```

You might deploy the components like this:

*   **API servers:** Run your Express application and accept HTTP requests.
    
*   **Redis:** Runs on a dedicated Redis server or managed Redis service.
    
*   **Worker servers:** Run `worker.js` independently and process background jobs.
    
*   **Load balancer:** Distributes incoming HTTP requests across API instances.
    
*   **Monitoring:** Tracks failed jobs, queue growth, processing time, and worker health.
    

The API and workers can be scaled independently. For example, you might run four API instances but only two workers during quiet periods, then add workers when background jobs increase.

### What changes when using Docker Compose?

For local development, Docker Compose can run Redis, the API, and the worker together.

In a real deployment, you might use separate containers for the API and workers, with Redis hosted independently.

If all three run inside Docker Compose, the API and worker can use the Redis service name `redis` to connect over the internal Docker network. You generally do not need to expose Redis's port publicly.

### What changes when using Kubernetes?

Kubernetes can manage the API and worker as separate Deployments. It can restart failed containers, distribute workloads, and scale application instances.

Redis can be provided by a managed service or deployed with suitable persistent storage and operational safeguards.

One important distinction: Kubernetes restarting a worker is not the same as BullMQ retrying a job. Kubernetes manages application processes and infrastructure; BullMQ manages job processing and recovery.

For scaling workers based on queue depth, you can use monitoring and an autoscaling mechanism such as KEDA, provided the required metrics and configuration are in place.

## 11\. Production Best Practices

Before deploying a BullMQ application to production, keep these points in mind.

*   **Configure Redis persistence.** Enable appropriate persistence and recovery settings. A Docker volume alone is not a complete backup strategy.
    
*   **Use** `noeviction`**.** BullMQ's Redis configuration should avoid arbitrary key eviction, which can break queue behavior.
    
*   **Protect Redis.** Use authentication, network restrictions, and encrypted connections where appropriate.
    
*   **Make jobs idempotent.** A retry should not cause duplicate payments, orders, or other unwanted effects.
    
*   **Handle errors.** Log queue and worker errors, and alert when failures increase.
    
*   **Close workers gracefully.** Allow active jobs to finish where possible before shutting down.
    
*   **Monitor queue growth.** If jobs arrive faster than workers can process them, waiting times will increase.
    
*   **Limit job data.** Pass only the information needed to process the job. Avoid placing passwords, API secrets, or unnecessary personal information in job payloads.
    
*   **Keep dependencies healthy.** Redis, the database, and external services all affect job-processing reliability.
    

For more details, see the official [BullMQ production guide](https://docs.bullmq.io/guide/going-to-production).

## 12\. Frequently Asked Interview Questions

**Q1. Why do we need Redis with BullMQ?**

Redis stores the queue's job data and state and allows producers and workers to coordinate through a shared backend.

**Q2. Can the producer and worker run on different servers?**

Yes. They need network access to the same Redis instance and must use the same queue name and compatible configuration.

**Q3. Does the producer wait until a job is completed?**

No. The producer normally waits for the job to be added to the queue, not for the background task to finish.

**Q4. What happens if no worker is running?**

Jobs can remain waiting in the queue until a worker becomes available, assuming Redis and the queue remain operational.

**Q5. Can multiple workers process jobs from the same queue?**

Yes. Multiple workers can process jobs concurrently, subject to their concurrency settings and available resources.

**Q6. What is the difference between concurrency and multiple workers?**

Concurrency allows a worker to process multiple jobs at the same time. Multiple workers use separate worker instances or processes to distribute processing and increase capacity.

**Q7. What happens when a job fails?**

BullMQ can retry it according to its configured attempt and backoff settings. After the allowed attempts are exhausted, it remains failed until an appropriate recovery or retry action is taken.

**Q8. Can BullMQ prevent duplicate job execution entirely?**

No. Jobs can be executed again in failure and recovery scenarios. Design important operations to be idempotent.

**Q9. Is Redis only a cache?**

No. Redis is also a general-purpose data store. In this architecture, it acts as the backend for BullMQ's queue operations.

**Q10. Why use a queue instead of making an HTTP request between services?**

A queue is useful when work can happen asynchronously, needs retries, or should be distributed across workers. HTTP is often better when a caller needs an immediate result.

## 13\. Final Summary

Remember these five components:

| Component | Remember it as |
| --- | --- |
| Producer | Adds jobs |
| Queue | Organizes background work |
| Redis | Stores and coordinates queue state |
| Worker | Processes jobs |
| BullMQ | Connects these pieces with queue-management features |

The complete flow is:

```text
Request
   |
   v
Producer creates a job
   |
   v
BullMQ stores queue state in Redis
   |
   v
Worker receives and processes the job
   |
   v
Job becomes completed or failed
```

The biggest lesson is that **a queue separates the act of requesting work from the act of performing it**. That separation helps applications handle slow tasks, retry failures, and scale background processing independently of their API servers.

Start with this small project, then try integrating a real email provider, adding a job-status endpoint, and running multiple workers. Those exercises will help turn the basic concept into practical backend engineering knowledge.

## Official References

1.  [BullMQ Quick Start](https://docs.bullmq.io/quick-start)
    
2.  [BullMQ Redis Connections](https://docs.bullmq.io/guide/connections)
    
3.  [BullMQ Workers and Concurrency](https://docs.bullmq.io/guide/workers/concurrency)
    
4.  [BullMQ Idempotent Jobs](https://docs.bullmq.io/patterns/idempotent-jobs)
    
5.  [BullMQ Production Guide](https://docs.bullmq.io/guide/going-to-production)
    
6.  [Docker Compose Documentation](https://docs.docker.com/compose/)
