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.
Set a rate limit
Section titled “Set a rate limit”Cap a queue at 100 jobs per second:
bunqueue rate-limit set emails 100 # max 100 jobs/secondbunqueue rate-limit clear emails # back to unlimitedOr from the SDK (works in both embedded and TCP mode):
queue.setGlobalRateLimit(100); // 100 jobs per secondqueue.setGlobalRateLimit(100, 60_000); // 100 jobs per minutequeue.removeGlobalRateLimit();
// Awaitable variants: resolve after the server applied the changeawait queue.setGlobalRateLimitAsync(100, 60_000);await queue.removeGlobalRateLimitAsync();await queue.setGlobalRateLimit(100); // 100 jobs per secondawait queue.setGlobalRateLimit(100, 60_000); // 100 jobs per minuteawait queue.removeGlobalRateLimit();queue.set_global_rate_limit(100) # 100 jobs per secondqueue.set_global_rate_limit(100, 60000) # 100 jobs per minutequeue.remove_global_rate_limit()$queue->setRateLimit(100); // 100 jobs per second$queue->setRateLimit(100, 60000); // 100 jobs per minute$queue->clearRateLimit();err := queue.SetRateLimit(100) // 100 jobs per seconderr = queue.SetRateLimit(100, bunqueue.RateLimitOptions{DurationMs: 60000}) // per minuteerr = queue.ClearRateLimit()queue.set_rate_limit(100, None, None)?; // 100 jobs per secondqueue.set_rate_limit(100, Some(60_000), None)?; // 100 jobs per minutequeue.clear_rate_limit()?;:ok = Bunqueue.Queue.set_rate_limit(queue, 100) # per second:ok = Bunqueue.Queue.set_rate_limit(queue, 100, duration: 60_000) # per minute:ok = Bunqueue.Queue.clear_rate_limit(queue)The limit is a token bucket that refills continuously: max tokens spread over the duration window (default 1 second).
Set a concurrency limit
Section titled “Set a concurrency limit”Cap a queue at 5 jobs running at the same time, across all workers:
bunqueue concurrency set emails 5bunqueue concurrency clear emailsqueue.setGlobalConcurrency(5);queue.removeGlobalConcurrency();await queue.setGlobalConcurrency(5);await queue.removeGlobalConcurrency();queue.set_global_concurrency(5)queue.remove_global_concurrency()The PHP SDK does not expose this broker command yet. Configure the same server-side limit with the CLI; it still applies to PHP workers:
bunqueue concurrency set emails 5bunqueue concurrency clear emailsThe Go SDK does not expose this broker command yet. Configure the same server-side limit with the CLI; it still applies to Go workers:
bunqueue concurrency set emails 5bunqueue concurrency clear emailsThe Rust SDK does not expose this broker command yet. Configure the same server-side limit with the CLI; it still applies to Rust workers:
bunqueue concurrency set emails 5bunqueue concurrency clear emails:ok = Bunqueue.Queue.set_concurrency(queue, 5):ok = Bunqueue.Queue.clear_concurrency(queue)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.
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});const worker = new Worker('emails', processor, { concurrency: 5, // this worker runs at most 5 jobs at once});worker = Worker("emails", process, concurrency=5) # at most 5 jobs at once// A PHP Worker is intentionally sequential. Run five worker processes when// this service should process up to five jobs in parallel.$worker = new Worker('emails', $processor);$worker->run();worker := bunqueue.NewWorker("emails", processor, bunqueue.WorkerOptions{ Concurrency: 5, // this worker runs at most 5 jobs at once})let worker = Worker::new("emails", processor, WorkerOptions { concurrency: 5, // this worker runs at most 5 jobs at once ..Default::default()});worker = Bunqueue.Worker.new("emails", processor, concurrency: 5)The PHP worker processes jobs sequentially by design and has no concurrency option; run more PHP worker processes to parallelize.
Custom time windows (per worker)
Section titled “Custom time windows (per worker)”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 network SDK has no per-worker limiter. Use a custom queue-wide broker window instead:
await queue.setGlobalRateLimit(100, 60_000); // 100 starts/min across all workersThe Python SDK has no per-worker limiter. Use a custom queue-wide broker window instead:
queue.set_global_rate_limit(100, 60000) # 100 starts/min across all workersThe PHP SDK supports a custom window as a queue-wide broker limit:
$queue->setRateLimit(100, 60000); // 100 starts/min across all workersThe Go SDK supports a custom window as a queue-wide broker limit:
err := queue.SetRateLimit(100, bunqueue.RateLimitOptions{DurationMs: 60000})The Rust SDK supports a custom window as a queue-wide broker limit:
queue.set_rate_limit(100, Some(60_000), None)?;The Elixir SDK supports a custom window as a queue-wide broker limit:
:ok = Bunqueue.Queue.set_rate_limit(queue, 100, duration: 60_000)The Bun 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 secondsThis override works with or without a configured limiter, including workers
using groupKey, and never alters tokens already consumed by previous starts.
The worker limiter option is available in the Bun client. In the network
SDKs, use the queue-level rate limit shown in each tab or a client-side limiter
of your own in the processor.
Using AI agents?
Section titled “Using AI agents?”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.
Reference
Section titled “Reference”| Control | Scope | Window | How |
|---|---|---|---|
| Rate limit | Queue (all workers) | Any duration (default 1s) | bunqueue rate-limit set, queue.setGlobalRateLimit(max, duration?) |
| Concurrency limit | Queue (all workers) | n/a | bunqueue concurrency set, queue.setGlobalConcurrency(n) |
| Worker concurrency | One worker | n/a | new Worker(..., { concurrency }) |
| Worker limiter | One worker | Rolling duration | new Worker(..., { limiter: { max, duration } }) |
| Temporary worker override | One worker | Explicit TTL | worker.rateLimit(ms) |
Gotchas
Section titled “Gotchas”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);});const worker = new Worker('emails', async (job) => { await ratelimit.limit('email-send'); // external limiter, e.g. Upstash await sendEmail(job.data);});def process(job): ratelimit.limit("email-send") # your external limiter send_email(job.data)
worker = Worker("emails", process)$worker = new Worker('emails', function (Bunqueue\Job $job) use ($ratelimit) { $ratelimit->limit('email-send'); // your external limiter sendEmail($job->data());});worker := bunqueue.NewWorker("emails", func(job *bunqueue.Job) (any, error) { ratelimit.Limit("email-send") // your external limiter return sendEmail(job.Data())}, bunqueue.WorkerOptions{})let worker = Worker::new( "emails", |job| { ratelimit.limit("email-send"); // your external limiter send_email(job.data()); Ok(Value::from(true)) }, WorkerOptions::default(),);worker = Bunqueue.Worker.new("emails", fn job -> :ok = RateLimit.limit("email-send") # your external limiter send_email(job.data) {:ok, %{sent: true}} end)