bunqueue Rate Limiting and Concurrency Control for Bun Job Queues
Processing jobs as fast as possible isn’t always the goal. External APIs have rate limits, databases have connection limits, and downstream services can be overwhelmed by burst traffic. bunqueue gives you fine-grained control over how fast and how many jobs run simultaneously.
Two Types of Limits
Section titled “Two Types of Limits”| Type | What It Controls | Use Case |
|---|---|---|
| Rate Limit | Jobs per time window | API rate limits (e.g., 100 req/min) |
| Concurrency Limit | Simultaneous active jobs | Database connections, CPU-bound tasks |
Rate Limiting
Section titled “Rate Limiting”Limit how many jobs are processed per time window:
const queue = new Queue('api-calls', { embedded: true });
// Max 100 jobs per minutequeue.setGlobalRateLimit(100, 60_000);
// Max 10 jobs per secondqueue.setGlobalRateLimit(10, 1_000);When the rate limit is hit, workers automatically pause and resume when the window resets. No jobs are lost - they just wait in the queue.
Worker-Side Rate Limiting
Section titled “Worker-Side Rate Limiting”Workers can also control their own rate:
const worker = new Worker('api-calls', processor, { embedded: true, concurrency: 5, limiter: { max: 50, // Max 50 jobs duration: 60_000, // Per minute },});Dynamic Rate Limiting
Section titled “Dynamic Rate Limiting”Adjust limits at runtime in response to API feedback:
const worker = new Worker('external-api', async (job) => { const response = await callExternalAPI(job.data);
// Check rate limit headers const remaining = response.headers.get('X-RateLimit-Remaining'); if (parseInt(remaining) < 10) { // Slow down - we're approaching the limit await worker.rateLimit(30_000); // Pause for 30 seconds }
return response.data;}, { embedded: true, concurrency: 3 });Concurrency Control
Section titled “Concurrency Control”Limit how many jobs run at the same time:
const queue = new Queue('heavy-processing', { embedded: true });
// Max 5 jobs active simultaneously across all workersqueue.setGlobalConcurrency(5);This is essential for:
- Database-heavy jobs - prevent connection pool exhaustion
- CPU-intensive tasks - prevent system overload
- Memory-intensive operations - prevent OOM kills
Worker Concurrency
Section titled “Worker Concurrency”Each worker also has its own concurrency setting:
// This worker processes up to 3 jobs at a timeconst worker = new Worker('tasks', processor, { embedded: true, concurrency: 3,});Global concurrency and worker concurrency work together:
- Global: 10 max across all workers
- Worker A: concurrency 5
- Worker B: concurrency 5
- If Worker A has 8 active, Worker B can only have 2
Combining Rate Limits and Concurrency
Section titled “Combining Rate Limits and Concurrency”For APIs with both rate limits and connection limits:
const queue = new Queue('stripe-api', { embedded: true });
// Stripe rate limit: 100 requests/secondqueue.setGlobalRateLimit(100, 1_000);
// But also limit concurrent connectionsqueue.setGlobalConcurrency(25);
const worker = new Worker('stripe-api', async (job) => { const result = await stripe.charges.create(job.data); return result;}, { embedded: true, concurrency: 10, // Per worker limit});Removing Limits
Section titled “Removing Limits”Clear limits when they’re no longer needed:
// Remove rate limitqueue.removeGlobalConcurrency();
// Check current stateconst isMaxed = await queue.isMaxed();const ttl = await queue.getRateLimitTtl();const isLimited = await worker.isRateLimited();Backpressure Patterns
Section titled “Backpressure Patterns”When downstream services are slow, queue depth grows. Here’s how to handle it:
Pattern 1: Monitor Queue Depth
Section titled “Pattern 1: Monitor Queue Depth”setInterval(async () => { const counts = await queue.getJobCountsAsync();
if (counts.waiting > 10_000) { console.warn('Queue backlog growing:', counts.waiting); // Consider: reduce producers, increase workers, alert team }}, 30_000);Pattern 2: Adaptive Concurrency
Section titled “Pattern 2: Adaptive Concurrency”const worker = new Worker('tasks', async (job) => { const startTime = Date.now(); const result = await processJob(job.data); const duration = Date.now() - startTime;
// If jobs are taking too long, reduce concurrency if (duration > 5_000) { queue.setGlobalConcurrency(Math.max(1, currentConcurrency - 1)); }
return result;}, { embedded: true, concurrency: 10 });Pattern 3: Circuit Breaker with DLQ
Section titled “Pattern 3: Circuit Breaker with DLQ”let consecutiveFailures = 0;
const worker = new Worker('external-api', async (job) => { try { const result = await callAPI(job.data); consecutiveFailures = 0; // Reset on success return result; } catch (err) { consecutiveFailures++;
if (consecutiveFailures > 10) { // Circuit breaker: pause the queue await queue.pause(); console.error('Circuit breaker triggered - queue paused');
// Resume after 60 seconds setTimeout(() => { queue.resume(); consecutiveFailures = 0; }, 60_000); }
throw err; // Job will retry or go to DLQ }}, { embedded: true, concurrency: 5 });Best Practices
Section titled “Best Practices”- Start conservative - begin with low concurrency and increase based on metrics
- Match external limits - if your API allows 100 req/min, set your rate limit to 80/min (leave headroom)
- Monitor queue depth - a growing backlog is the first sign of trouble
- Use global concurrency for shared resources - database connections, API quotas
- Use worker concurrency for CPU/memory - prevent any single worker from consuming too many resources
- Implement circuit breakers for external dependencies - pause queues when downstream is unhealthy