Skip to content
Get started
Get started
Rate Limiting & Concurrency for Bun Job Queues
View Markdown
guide · rate limiting

Rate limits and concurrency, under control.

Cap how many jobs start per second, or how many run at the same time, so a busy queue never overwhelms the API or database behind it.

bunqueue gives you two independent knobs per queue:

  • Rate limit: how many jobs may start per second (throughput cap).
  • Concurrency limit: how many jobs may be active at once (parallelism cap).

Neither is set by default, so queues run unlimited until you say otherwise.

Cap a queue at 100 jobs per second:

bunqueue rate-limit set emails 100 # max 100 jobs/second
bunqueue rate-limit clear emails # back to unlimited

Or from the SDK (works in both embedded and TCP mode):

queue.setGlobalRateLimit(100); // 100 jobs per second
queue.setGlobalRateLimit(100, 60_000); // 100 jobs per minute
queue.removeGlobalRateLimit();
// Awaitable variants: resolve after the server applied the change
await queue.setGlobalRateLimitAsync(100, 60_000);
await queue.removeGlobalRateLimitAsync();

The limit is a token bucket that refills continuously: max tokens spread over the duration window (default 1 second).

Cap a queue at 5 jobs running at the same time, across all workers:

bunqueue concurrency set emails 5
bunqueue concurrency clear emails
queue.setGlobalConcurrency(5);
queue.removeGlobalConcurrency();

The PHP, Go, and Rust SDKs do not expose the global concurrency helpers yet. The cap lives server-side per queue, so set it with bunqueue concurrency set or from any other client and it applies to workers in every language.

Jobs that are already running when you set the limit count toward it, as in BullMQ. If 3 jobs of emails are active and you set the limit to 5, only 2 more start until running jobs finish. The same holds when you clear the limit and set it again. Lowering the limit below the number of running jobs never interrupts them; new jobs start only once the active count drops below the new limit.

This is a queue-level cap. Each worker also has its own concurrency option that limits how many jobs that one worker runs in parallel:

const worker = new Worker('emails', processor, {
concurrency: 5, // this worker runs at most 5 jobs at once
});

The PHP worker processes jobs sequentially by design and has no concurrency option; run more PHP worker processes to parallelize.

The queue-level limit above already supports any window via the duration argument. If you instead want the cap enforced per single worker, use the limiter option:

const worker = new Worker('emails', processor, {
limiter: { max: 100, duration: 60_000 }, // 100 jobs per minute, per worker
});

The TypeScript worker limit is enforced client-side by each worker, so with 3 identical workers the effective rate is 3x.

The budget counts job starts, not completions. Admission is synchronous at the processor dispatch boundary, so concurrency: 20 cannot overshoot a max: 2 window. Batch pulling is capped by the same remaining budget: jobs beyond those two starts stay waiting on the broker instead of being leased and parked inside the worker.

The same gate applies to getNextJob() + processJobManually(). Manual processing waits until a start token is available before invoking the processor. When locks are enabled, getNextJob() exposes the broker lease on job.token, and processJobManually(job) reuses that tracked token if its explicit token argument is omitted.

You can also apply a temporary worker-local pause dynamically:

worker.rateLimit(5_000); // do not start another job for at least five seconds

This override works with or without a configured limiter, including workers using groupKey, and never alters tokens already consumed by previous starts. ms is a number of milliseconds. As in 2.9.10 and BullMQ, a value that is not a positive finite number (0, a negative value, NaN, Infinity, null) does nothing, and a numeric string such as '5000' is read as that number; any other non-number throws a TypeError. A long override is honoured exactly: the worker starts no job until it ends.

The worker limiter option is available in both TypeScript packages. In the other SDKs, use the queue-level rate limit shown in each tab or a client-side limiter of your own in the processor.

Agents connected via MCP can set and clear both limits in natural language (“rate limit emails to 50 per second”) through the bunqueue_set_rate_limit, bunqueue_clear_rate_limit, bunqueue_set_concurrency, and bunqueue_clear_concurrency tools.

ControlScopeWindowHow
Rate limitQueue (all workers)Any duration (default 1s)bunqueue rate-limit set, queue.setGlobalRateLimit(max, duration?)
Concurrency limitQueue (all workers)n/abunqueue concurrency set, queue.setGlobalConcurrency(n)
Worker concurrencyOne workern/anew Worker(..., { concurrency })
Worker limiterOne workerRolling durationnew Worker(..., { limiter: { max, duration } })
Temporary worker overrideOne workerExplicit TTL (a value that is not a positive finite number is a no-op)worker.rateLimit(ms)

For rate limiting that must be shared with code outside the queue (for example, an API budget also consumed by web requests), use an external limiter inside your processor:

const worker = new Worker('emails', async (job) => {
await ratelimit.limit('email-send'); // external limiter, e.g. Upstash
await sendEmail(job.data);
});