npm install bullmq
// queue.js — define the queue
const { Queue } = require('bullmq');
const imageQueue = new Queue(
'image-processing', {
connection: {
host: 'localhost',
port: 6379
}
});
module.exports = { imageQueue };
const { imageQueue } = require('./queue');
app.post('/api/images/upload',
authenticate, upload.single('image'),
async (req, res) => {
// Save image record immediately
const image = await Image.create({
owner: req.userId,
originalPath: req.file.path,
status: 'processing',
});
// Enqueue background job
const job = await imageQueue.add(
'process-image',
{
imageId: image.id,
filePath: req.file.path,
userId: req.userId,
},
{
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
}
);
// Respond immediately — don't wait
res.status(202).json({
image,
jobId: job.id,
message: 'Upload received, ' +
'processing in background',
});
});
// worker.js — separate process!
const { Worker } = require('bullmq');
const sharp = require('sharp');
const worker = new Worker(
'image-processing',
async (job) => {
const { imageId, filePath } = job.data;
// Step 1: Generate thumbnail
await job.updateProgress(25);
await sharp(filePath)
.resize(200, 200, { fit: 'cover' })
.webp({ quality: 80 })
.toFile(`thumbnails/${imageId}.webp`);
// Step 2: Generate preview
await job.updateProgress(50);
await sharp(filePath)
.resize(800)
.webp({ quality: 85 })
.toFile(`previews/${imageId}.webp`);
// Step 3: Extract metadata
await job.updateProgress(75);
const metadata =
await sharp(filePath).metadata();
// Step 4: Update database
await Image.findByIdAndUpdate(
imageId, {
status: 'ready',
thumbnailPath:
`thumbnails/${imageId}.webp`,
previewPath:
`previews/${imageId}.webp`,
width: metadata.width,
height: metadata.height,
format: metadata.format,
});
await job.updateProgress(100);
return { imageId, status: 'ready' };
},
{ connection: {
host: 'localhost', port: 6379 } }
);
worker.on('completed', (job, result) => {
console.log(
`Job ${job.id} completed:`,
result);
});
worker.on('failed', (job, err) => {
console.error(
`Job ${job.id} failed:`, err.message);
});
// API: check job status
app.get('/api/jobs/:jobId',
authenticate, async (req, res) => {
const job = await imageQueue.getJob(
req.params.jobId);
if (!job) {
return res.status(404).json({
error: 'Job not found' });
}
const state = await job.getState();
const progress = job.progress;
res.json({ state, progress });
});
// Client: poll for status
function useJobStatus(jobId) {
const [status, setStatus]
= useState('waiting');
const [progress, setProgress]
= useState(0);
useEffect(() => {
const interval = setInterval(
async () => {
const res = await fetch(
`/api/jobs/${jobId}`);
const data = await res.json();
setStatus(data.state);
setProgress(data.progress);
if (data.state === 'completed'
|| data.state === 'failed') {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, [jobId]);
return { status, progress };
}
// Run every hour: clean temp files
await imageQueue.add(
'cleanup-temp-files',
{},
{
repeat: {
pattern: '0 * * * *' // every hour
}
}
);
git switch -c feature/PIXELCRAFT-066-background-jobs
git add server/
git commit -m "Add BullMQ background processing for uploads (PIXELCRAFT-066)"
git push origin feature/PIXELCRAFT-066-background-jobs
# PR → Review → Merge → Close ticket ✅
Every production system separates fast work from slow work.
The pattern is message queues — one of the most important concepts in distributed systems architecture.