Best Monorepo Setup for Long-Running Background Jobs (Simple Guide)
Learn the simplest monorepo setup for running long background jobs using BullMQ and Node.js with PM2. Clean, scalable, and beginner-friendly.

Best Monorepo Setup for Long-Running Background Jobs (Simple Guide)
Looking for a simple monorepo setup for long-running background jobs?
Here’s a beginner-friendly guide that actually works without hassle.
Introduction
Managing long-running background jobs in a monorepo can be tricky, especially when dealing with concurrency, job queues, and system resources. If you're struggling to find a clean solution that doesn't require hours of setup or complicated tooling, you're not alone.
In this article, we’ll walk through a simple, efficient monorepo structure that can run concurrent jobs for hours, handle database updates, and offer a lightweight dashboard—no fancy tools required.
Why Monorepos Make Sense for Background Jobs
A monorepo helps you keep multiple apps and packages in one place—ideal for tightly coupled services like:
API servers
Job queues
Workers for long-running tasks
Shared utilities and configs
This setup can make deployments faster and local development smoother.
Common Challenges in Long-Running Job Setups
Let’s talk about the pain points developers face:
Jobs failing with "stalled" errors
BullMQ Dashboard lagging or unresponsive
Trigger.dev complexity for small teams
Overkill infrastructure for simple use-cases
Despite using powerful machines (32 vCPU, 32 GB RAM), many find jobs failing without clear reasons. If you're seeing errors like job stalled more than allowable limit, you're likely hitting heartbeat or concurrency issues, not hardware limits.
The Ideal Simple Setup (Without Complex Tools)
Here's a clean and functional setup using only:
Node.js + Express
BullMQ
Redis
PM2 (for process management)
All inside a monorepo.
1. Folder Structure
my-monorepo/
├── apps/
│ ├── api/
│ └── worker/
├── packages/
│ ├── db/
│ └── utils/
2. Worker App with BullMQ
import { Worker } from 'bullmq';
import IORedis from 'ioredis';
const connection = new IORedis();
const worker = new Worker('long-jobs', async job => {
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 60 * 2)); // 2hr task
}, {
concurrency: 5,
connection,
lockDuration: 7200000
});
worker.on('completed', job => console.log(`Job ${job.id} completed`));
worker.on('failed', (job, err) => console.error(`Job ${job?.id} failed`, err));
3. PM2 Setup
module.exports = {
apps: [
{ name: 'api', script: 'apps/api/index.js' },
{ name: 'worker', script: 'apps/worker/index.js' }
]
};
Use:
npx pm2 start ecosystem.config.js
4. Lightweight Dashboards
Or your own Express route with job status
5. Prevent Stalled Jobs
Match lockDuration with job duration
Use async/await properly
Stable Redis connection
Avoid blocking code
✅ Summary
If you're tired of over-engineered solutions, a basic Node + BullMQ worker in a monorepo is all you need. Here's why:
Clean separation of concerns
Minimal tools, easy monitoring
Scalable with PM2
Flexible with Redis
🚀 Ready to Simplify?
Ditch the complexity and start small. Share this guide with your dev team or bookmark it for your next build.
Explore more tech tips at TrendPulseZone.com.