# Rate Limiting & Concurrency for Bun Job Queues

Control job processing rates in bunqueue with per-queue rate limits and concurrency caps. Protect downstream services via CLI, SDK or MCP.

Canonical: https://bunqueue.dev/guide/rate-limiting/

---

import { Tabs, TabItem } from '@astrojs/starlight/components';

<div class="bq-wrap bq-hero">
  <span class="bq-eyebrow">guide · rate limiting</span>
  <h1 class="bq-hero-h1 bq-bench-h1">Rate limits and concurrency, under <em>control.</em></h1>
  <p class="bq-hero-sub">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.</p>
</div>

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

Cap a queue at 100 jobs per second:

```bash
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):

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
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();
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
await queue.setGlobalRateLimitAsync(100);          // 100 jobs per second
await queue.setGlobalRateLimitAsync(100, 60_000);  // 100 jobs per minute
await queue.removeGlobalRateLimitAsync();

// Awaitable variants: resolve after the server applied the change
await queue.setGlobalRateLimitAsync(100, 60_000);
await queue.removeGlobalRateLimitAsync();
```

</TabItem>
<TabItem label="Python">

```python
queue.set_global_rate_limit(100)         # 100 jobs per second
queue.set_global_rate_limit(100, 60000)  # 100 jobs per minute
queue.remove_global_rate_limit()
```

</TabItem>
<TabItem label="PHP">

```php
$queue->setRateLimit(100);           // 100 jobs per second
$queue->setRateLimit(100, 60000);    // 100 jobs per minute
$queue->clearRateLimit();
```

</TabItem>
<TabItem label="Go">

```go
err := queue.SetRateLimit(100)       // 100 jobs per second
err = queue.SetRateLimit(100, bunqueue.RateLimitOptions{DurationMs: 60000}) // per minute
err = queue.ClearRateLimit()
```

</TabItem>
<TabItem label="Rust">

```rust
queue.set_rate_limit(100, None, None)?;          // 100 jobs per second
queue.set_rate_limit(100, Some(60_000), None)?;  // 100 jobs per minute
queue.clear_rate_limit()?;
```

</TabItem>
<TabItem label="Elixir">

```elixir
: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)
```

</TabItem>
</Tabs>

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

## Set a concurrency limit

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

```bash
bunqueue concurrency set emails 5
bunqueue concurrency clear emails
```

<Tabs syncKey="lang">
<TabItem label="Bun">

```typescript
queue.setGlobalConcurrency(5);
queue.removeGlobalConcurrency();
```

</TabItem>
<TabItem label="Node.js / Deno">

```typescript
await queue.setGlobalConcurrencyAsync(5);
await queue.removeGlobalConcurrencyAsync();
```

</TabItem>
<TabItem label="Python">

```python
queue.set_global_concurrency(5)
queue.remove_global_concurrency()
```

</TabItem>
<TabItem label="PHP">

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:

```bash
bunqueue concurrency set emails 5
bunqueue concurrency clear emails
```

</TabItem>
<TabItem label="Go">

The Go SDK does not expose this broker command yet. Configure the same
server-side limit with the CLI; it still applies to Go workers:

```bash
bunqueue concurrency set emails 5
bunqueue concurrency clear emails
```

</TabItem>
<TabItem label="Rust">

The Rust SDK does not expose this broker command yet. Configure the same
server-side limit with the CLI; it still applies to Rust workers:

```bash
bunqueue concurrency set emails 5
bunqueue concurrency clear emails
```

</TabItem>
<TabItem label="Elixir">

```elixir
:ok = Bunqueue.Queue.set_concurrency(queue, 5)
:ok = Bunqueue.Queue.clear_concurrency(queue)
```

</TabItem>
</Tabs>

*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:

<Tabs syncKey="lang">
<TabItem label="Bun">

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

</TabItem>
<TabItem label="Node.js / Deno">

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

</TabItem>
<TabItem label="Python">

```python
worker = Worker("emails", process, concurrency=5)  # at most 5 jobs at once
```

</TabItem>
<TabItem label="PHP">

```php
// 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();
```

</TabItem>
<TabItem label="Go">

```go
worker := bunqueue.NewWorker("emails", processor, bunqueue.WorkerOptions{
    Concurrency: 5, // this worker runs at most 5 jobs at once
})
```

</TabItem>
<TabItem label="Rust">

```rust
let worker = Worker::new("emails", processor, WorkerOptions {
    concurrency: 5, // this worker runs at most 5 jobs at once
    ..Default::default()
});
```

</TabItem>
<TabItem label="Elixir">

```elixir
worker = Bunqueue.Worker.new("emails", processor, concurrency: 5)
```

</TabItem>
</Tabs>

*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)

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:

<Tabs syncKey="lang">
<TabItem label="Bun">

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

</TabItem>
<TabItem label="Node.js / Deno">

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

</TabItem>
<TabItem label="Python">

The Python SDK has no per-worker limiter. Use a custom queue-wide broker window
instead:

```python
queue.set_global_rate_limit(100, 60000)  # 100 starts/min across all workers
```

</TabItem>
<TabItem label="PHP">

The PHP SDK supports a custom window as a queue-wide broker limit:

```php
$queue->setRateLimit(100, 60000); // 100 starts/min across all workers
```

</TabItem>
<TabItem label="Go">

The Go SDK supports a custom window as a queue-wide broker limit:

```go
err := queue.SetRateLimit(100, bunqueue.RateLimitOptions{DurationMs: 60000})
```

</TabItem>
<TabItem label="Rust">

The Rust SDK supports a custom window as a queue-wide broker limit:

```rust
queue.set_rate_limit(100, Some(60_000), None)?;
```

</TabItem>
<TabItem label="Elixir">

The Elixir SDK supports a custom window as a queue-wide broker limit:

```elixir
:ok = Bunqueue.Queue.set_rate_limit(queue, 100, duration: 60_000)
```

</TabItem>
</Tabs>

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:

```typescript
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.

*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?

Agents connected via [MCP](/guide/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

| 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

:::note[Older servers ignore the duration]
Servers older than 2.8.35 ignore the `duration` argument and always use a 1-second window. Upgrade the server to get custom windows; the client remains compatible in both directions.
:::

:::note[Temporary throttle expires server-side]
`await queue.rateLimit(ms)` throttles the queue to ~1 job/sec and the **server** clears it after `ms` on its own, in embedded and TCP mode alike. It throws if `ms` is not a positive finite number.
:::

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:

<Tabs syncKey="lang">
<TabItem label="Bun">

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

</TabItem>
<TabItem label="Node.js / Deno">

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

</TabItem>
<TabItem label="Python">

```python
def process(job):
    ratelimit.limit("email-send")  # your external limiter
    send_email(job.data)

worker = Worker("emails", process)
```

</TabItem>
<TabItem label="PHP">

```php
$worker = new Worker('emails', function (Bunqueue\Job $job) use ($ratelimit) {
    $ratelimit->limit('email-send'); // your external limiter
    sendEmail($job->data());
});
```

</TabItem>
<TabItem label="Go">

```go
worker := bunqueue.NewWorker("emails", func(job *bunqueue.Job) (any, error) {
    ratelimit.Limit("email-send") // your external limiter
    return sendEmail(job.Data())
}, bunqueue.WorkerOptions{})
```

</TabItem>
<TabItem label="Rust">

```rust
let worker = Worker::new(
    "emails",
    |job| {
        ratelimit.limit("email-send"); // your external limiter
        send_email(job.data());
        Ok(Value::from(true))
    },
    WorkerOptions::default(),
);
```

</TabItem>
<TabItem label="Elixir">

```elixir
worker =
  Bunqueue.Worker.new("emails", fn job ->
    :ok = RateLimit.limit("email-send")  # your external limiter
    send_email(job.data)
    {:ok, %{sent: true}}
  end)
```

</TabItem>
</Tabs>

:::tip[Related Guides]
- [Queue API](/guide/queue/) - Queue configuration options
- [Worker API](/guide/worker/) - Worker concurrency settings
- [Environment Variables](/guide/env-vars/) - Protocol-level per-client request limiter (`RATE_LIMIT_*`), separate from queue rate limits
:::