Lesson 4 — Priority & Rate Limiting

Goal: Control order (which job runs first) and throughput (how many jobs run per unit of time). Priority answers "VIP first"; rate limiting answers "don't get us blocked by the API we're calling."

Tied to mission: real queues aren't FIFO-only. Support tickets have SLAs; third-party APIs return 429. Builds on Lesson 1 and Lesson 2.


Part A — Priority: who runs first

Every job can carry a priority. BullMQ pulls higher-priority jobs before lower ones instead of strict FIFO.

await queue.add('wall', { color: 'pink'  }, { priority: 10 });
await queue.add('wall', { color: 'brown' }, { priority: 5  });
await queue.add('wall', { color: 'blue'  }, { priority: 7  });
// processed order: brown (5), blue (7), pink (10)

The rules

RuleDetail
Lower number = higher priorityCounter-intuitive. 1 beats 10. Range: 1 to 2 097 152.
No priority = highestJobs with no priority jump ahead of all prioritized jobs. Don't mix thoughtlessly.
Ties are FIFOSame priority value → first-in-first-out within that tier.
Adding is O(log n)Prioritized inserts are slower than normal ones (sorted set). Don't set priority on every job — only the ones that matter.

Change priority after insert

const job = await queue.add('wall', { color: 'red' }, { priority: 9 });
await job.changePriority({ priority: 1 });  // bump to front

Inspect

const counts = await queue.getCountsPerPriority(0, 1, 5, 10);
// { '0': 140, '1': 3, '5': 12, '10': 8 }

0 = jobs with no priority assigned.

Priority is not preemption. It only affects which waiting job is picked next when a worker becomes free. A low-priority job already active is never preempted by a higher-priority arrival. To cut something off, you'd pause the queue and re-add — don't reach for that lightly.

Part B — Rate limiting: how fast they run

Rate limiting protects the thing your jobs call. Three layers in current BullMQ. Learn all three — they answer different questions.

1. Worker limiter (static, global)

new Worker('email', processor, {
  connection,
  limiter: { max: 10, duration: 1000 },  // 10 jobs / 1000ms
});
PropertyMeaning
maxMax jobs to process in the window.
durationThe window length, in ms.
It is GLOBAL across all workers on the queue. 10 workers, each with {max:10, duration:1000} → still 10 jobs/sec total, not 100. The limit is shared, not per-worker.

Jobs that would breach the limit stay in waiting — they are not failed, not lost.

2. Queue global rate limit (queue-level cap)

Set from the queue side, independent of any worker's config. Useful when multiple worker pools or producers must respect one ceiling.

await queue.setGlobalRateLimit(100, 60_000);   // 100 jobs/min, queue-wide
const { max, duration } = await queue.getGlobalRateLimit();
const ttl = await queue.getRateLimitTtl();      // >0 means currently throttled
await queue.removeGlobalRateLimit();
Worker limiter and global limiter coexist. Neither overrides the other — the stricter wins at any moment. Setting both by accident will silently slow you down.

3. Manual / dynamic rate limiting (for HTTP 429)

Static limits are guesses. When the downstream API itself says "slow down" (a 429 Too Many Requests with a Retry-After), act on it at runtime:

const worker = new Worker('email', async (job) => {
  const [ok, retryAfterMs] = await callEmailApi(job.data);
  if (!ok) {
    await worker.rateLimit(retryAfterMs);   // hold this queue for that long
    throw Worker.RateLimitError();            // MUST throw this, not a normal error
  }
}, {
  connection,
  limiter: { max: 5, duration: 1000 },
});
The two lines are a matched pair — never split them. worker.rateLimit(ms) tells BullMQ "hold the queue"; throw Worker.RateLimitError() puts this job back to waiting without consuming a retry attempt. If you threw a normal Error instead, BullMQ would treat it as a failure (Lesson 2) and burn through attempts — exactly wrong.

Inspect/clear manual limits:

const ttl = await queue.getRateLimitTtl();   // ms remaining on a manual hold; 0 = free
await queue.removeRateLimitKey();            // force-clear a manual hold

The groupKey trap — read this before you copy any tutorial

Older BullMQ (and Bull v3) supported limiter: { groupKey: 'customerId' } to rate-limit per tenant. This was removed in BullMQ 3.0. The RateLimiterOptions type now contains only { max, duration } — verified against the current source. Some docs pages still show groupKey in examples; they are stale. If you need per-tenant throttling today, use one queue per tenant, or a manual worker.rateLimit() keyed on the tenant inside your processor.

Full picture — VIP emails, throttled API

import { Queue, Worker } from 'bullmq';
import IORedis from 'ioredis';
const connection = new IORedis({ maxRetriesPerRequest: null });

type Mail = { to: string; tenant: string; vip: boolean };

const email = new Queue<Mail>('email', { connection });

// VIPs jump the queue
await email.add('send', { to: '[email protected]', tenant: 'acme', vip: true }, { priority: 1 });
await email.add('send', { to: '[email protected]',  tenant: 'acme', vip: false });

new Worker<Mail>('email', async (job) => {
  const [ok, retryAfter] = await provider.send(job.data);
  if (!ok) {
    await worker.rateLimit(retryAfter ?? 5000);
    throw Worker.RateLimitError();
  }
}, {
  connection,
  concurrency: 5,
  limiter: { max: 20, duration: 1000 },   // ≤ 20 emails/sec globally
});
Tangible win. You can now make the important job go first (priority) and protect the fragile dependency from your own traffic (rate limit — static, global, or dynamic on 429). Together with retries and scheduling, this covers the mechanics of nearly every production queue.

What you just learned


🧠 Quiz — 10 questions

1. Priority 5 vs priority 10 — which runs first?

2. A job with NO priority gets what treatment?

3. What is the cost of adding a prioritized job?

4. Does priority preempt a job already active?

5. 10 workers, each {max:10,duration:1000}. Total rate?

6. Where does a rate-limited job sit?

7. After worker.rateLimit(ms), what must you throw?

8. What does Worker.RateLimitError() prevent?

9. Is groupKey supported in current BullMQ?

10. Worker limiter vs global queue limit?


📚 Primary sources

Prioritized · Rate limiting · Global Rate Limit (official). Also the authoritative type: rate-limiter-options.ts.

📎 Reference

Priority & Rate Limiting Cheatsheet — one-screen lookup.

💬 Ask me next

Or say "next" for Lesson 5: flows (parent/child job dependencies).