Recommended Free Tools
With BullMQ you can run a gallery-sized batch (resizing, watermarking or zipping hundreds of event photos) as a queued job. A worker processes it asynchronously and reports progress with job.updateProgress. Clients get a stable job ID to look up current state, and a QueueEvents listener streams live changes across all workers. Cancellation works through an AbortSignal that your code must actually honor. The gallery scenario below is an illustration. The behavior described comes from BullMQ’s documentation on Workers, Events, Cancelling Jobs and the Job API.
How the pieces fit together
- Queue and job: your API adds a job (for example, “process gallery 4821”) and returns its ID immediately instead of making the HTTP request wait.
- Worker: runs an asynchronous processor. If the processor finishes, BullMQ moves the job to completed. If it throws, the job moves to failed, and may be retried if you configured attempts.
- Progress: the processor calls
job.updateProgresswith a number or a JSON-serializable object. - QueueEvents: a Redis-stream-based listener that sees lifecycle and progress events from every worker.
- Status lookup: an API endpoint keyed by job ID that reads the job’s current state on demand.
Step 1: Model a batch as one job with structured progress
Create a job for a bounded unit of work. If one job covers a whole gallery, define a progress object clients can rely on, with stable fields: completed count, total count and a short phase label. Keep internal data (file paths, storage credentials, stack details) out of it, because this object will likely reach browsers.
As an Amazon Associate I earn from qualifying purchases.
import { Queue, Worker } from 'bullmq';
const connection = { host: '127.0.0.1', port: 6379 };
export const galleryQueue = new Queue('gallery-processing', { connection });
// API handler
const job = await galleryQueue.add('process-gallery', { galleryId: 4821, photoIds });
// respond with { jobId: job.id }
// Worker
const worker = new Worker('gallery-processing', async (job, token, signal) => {
const total = job.data.photoIds.length;
for (let i = 0; i < total; i++) {
signal?.throwIfAborted();
await processPhoto(job.data.photoIds[i], { signal });
await job.updateProgress({ completed: i + 1, total, phase: 'resizing' });
}
return { processed: total };
}, { connection });
The processor receives an optional AbortSignal, hence the signal?. check. Whether it is supplied depends on your BullMQ version, so confirm against the version you have installed.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Step 2: Give callers a status endpoint
Return a status resource keyed by the job ID and query current queue state when a client asks. A typical handler fetches the job, reads its state, and returns the state, the latest progress value and, once finished, the return value or failure reason.
#1 Best Overall
const job = await galleryQueue.getJob(req.params.id);
if (!job) return res.sendStatus(404);
res.json({
state: await job.getState(),
progress: job.progress,
result: job.returnvalue,
failedReason: job.failedReason
});
Live events complement this lookup; they do not replace it.
Step 3: Push live updates with QueueEvents
Listeners attached to a Worker are local to the worker that handled the job. If your API process or dashboard service is separate, or you run several workers, use QueueEvents, which BullMQ documents as the cross-worker option.
Rank #2
import { QueueEvents } from 'bullmq';
const queueEvents = new QueueEvents('gallery-processing', { connection });
queueEvents.on('progress', ({ jobId, data }) => {
broadcast(jobId, { type: 'progress', data }); // WebSocket or SSE
});
queueEvents.on('completed', ({ jobId, returnvalue }) => broadcast(jobId, { type: 'completed' }));
queueEvents.on('failed', ({ jobId, failedReason }) => broadcast(jobId, { type: 'failed' }));
If a request must simply block until one job ends, the Job API offers job.waitUntilFinished(queueEvents). For long galleries, prefer returning the ID and letting the client poll or subscribe.
Free tools Windows power users keep installed
One-click scans. No signup required.
Events are not an audit log
BullMQ documents QueueEvents as Redis-stream based, with the stream automatically trimmed to approximately 10,000 events by default (the maximum is configurable). Two consequences follow:
Rank #3
- A client that reconnects should fetch current state from the status endpoint, not assume every past event is still available.
- Persist business-critical history (who published which gallery, when) in your own database.
Step 4: Cancel work that is running
Cancellation is cooperative. The worker hands the processor an optional AbortSignal, but nothing stops unless your processor and the operations it starts respond to that signal. Calling a cancel method on the worker for an active job (recent BullMQ versions expose these; see the Cancelling Jobs page for the exact methods in yours) only triggers the signal.
- Check
signal.abortedor callsignal.throwIfAborted()at safe points, such as between photos. - Pass the signal to APIs that accept one (
fetch, many HTTP and file-streaming libraries). - For custom operations, add an
abortlistener that really stops the work, for example destroying a stream or killing a child process. - Release files, sockets and database clients before rejecting.
function resizeWithAbort(photo, signal) {
return new Promise((resolve, reject) => {
const task = startResize(photo);
const onAbort = () => { task.cancel(); cleanup(task); reject(signal.reason); };
signal.addEventListener('abort', onAbort, { once: true });
task.done.then(resolve, reject)
.finally(() => signal.removeEventListener('abort', onAbort));
});
}
Decide whether cancellation should retry
The error you throw matters. In BullMQ’s documented pattern, a normal error can be retried if attempts remain, while throwing UnrecoverableError prevents a retry. For a user pressing “Cancel”, you almost always want the terminal behavior:
Rank #4
import { UnrecoverableError } from 'bullmq';
if (signal?.aborted) {
await cleanup();
throw new UnrecoverableError('Cancelled by user');
}
Then reflect this in your API: expose a distinct “cancelled” status to clients, since BullMQ itself will record the job as failed.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Failure modes to plan for
| Problem | Cause | What to do |
|---|---|---|
| Cancel accepted, work continues | Abort signal is not linked to a real cancellation mechanism | Check the signal in loops and attach abort handlers to custom operations |
| Cancelled job runs again | A regular error was thrown while attempts remained | Throw UnrecoverableError for terminal cancellation |
| Dashboard misses completions | Listening to local worker events only | Use QueueEvents |
| Reconnecting client has gaps | Event stream is trimmed (about 10,000 events by default) | Refetch state by job ID on reconnect |
| Leaked handles after cancel | Cleanup skipped on the abort path | Close files, sockets and clients before rejecting |
| Redis connection lingers on deploy | QueueEvents not closed |
Call await queueEvents.close() during shutdown |
What this does not establish
BullMQ’s documentation does not give deployment-specific throughput figures, application-level exactly-once guarantees, or comparisons with other queue libraries, so none are claimed here. If you evaluate alternatives, compare them on: backend dependency, how job state is queried, local versus global events, progress shape and persistence, cancellation propagation and cleanup duties, retry semantics on cancellation, and retention of history.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




