Skip to content
Get started
Get started
Job Groups: FIFO, Fairness and Backpressure
guide · queue

Fair streams, one queue.

Put each tenant, webhook destination or customer in a group. bunqueue keeps FIFO ties inside that stream, rotates fairly across streams and enforces backpressure at the broker.

The Bun client supports the group API in embedded and TCP mode:

import { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('webhooks');
await queue.add('deliver', { event: 'created' }, { group: { id: 'tenant-a' } });
await queue.add('deliver', { event: 'updated' }, { group: { id: 'tenant-a' } });
await queue.add('deliver', { event: 'created' }, { group: { id: 'tenant-b' } });

When both groups are ready, claims rotate tenant-a, tenant-b, tenant-a. Equal-priority jobs inside each group stay FIFO. Ready jobs without a group are served before grouped jobs.

Group IDs may be non-empty strings or safe integers and may contain at most 256 characters. NUL characters are rejected. Numeric IDs are normalized to strings; objects and fractional or unsafe numeric IDs are not coerced.

Use priority for ordered work inside one group and maxSize for atomic producer backpressure. Priority 0 is highest; among positive priorities, a lower number runs first. Values must be integers from 0 through 2,097,151:

await queue.add('deliver', payload, {
group: { id: 'tenant-a', priority: 1, maxSize: 10_000 },
});

maxSize counts pending jobs in waiting, prioritized, and delayed states; an active job no longer consumes that pending-cap slot. The broker performs the count and insert as one admission decision. A full group rejects a single add or the complete atomic FlowProducer graph without leaving partial jobs, and PostgreSQL addBulk is one transaction. The embedded (memory/SQLite) addBulk instead follows the accepted-prefix contract: jobs before the one that hits the cap are admitted and persisted, then the rejection is thrown.

FIFO controls which job is claimed first. By default, multiple jobs from the same group may execute at once. Set the broker-side group concurrency when you need serial or bounded work per tenant:

const worker = new Worker('webhooks', deliverWebhook, {
concurrency: 50,
group: {
concurrency: 2, // at most two active jobs per group
},
});

Use concurrency: 1 for strictly serial execution per group. The ordinary Worker concurrency still caps total parallel work in this process.

group.limit gives every group its own fixed window, enforced when the broker claims a job:

const worker = new Worker('webhooks', deliverWebhook, {
concurrency: 100,
group: {
limit: { max: 20, duration: 1000 }, // 20 starts/s for each group
},
});

The budget is shared by all Workers. In PostgreSQL multi-broker mode it is stored and consumed transactionally in PostgreSQL, so several brokers still share one exact budget per group.

All Workers consuming the queue should use the same group defaults. A Worker that omits group.limit has no group rate limiting; one that omits group.concurrency has unlimited group concurrency.

Local overrides let one tenant use a different policy without changing the Worker defaults:

await queue.setGroupRateLimit('tenant-a', 5, 1000);
await queue.setGroupConcurrency('tenant-a', 1);
console.log(await queue.getGroupRateLimit('tenant-a'));
// { max: 5, duration: 1000 }
console.log(await queue.getGroupConcurrency('tenant-a'));
// 1

An override is effective only when the Worker supplied the corresponding default. This matches BullMQ Pro’s local group override behavior: a stored rate override does not turn rate limiting on by itself, and a stored concurrency override does not turn group concurrency on by itself.

Remove overrides with:

await queue.removeGroupRateLimit('tenant-a');
await queue.removeGroupConcurrency('tenant-a');

Pause and resume one tenant without pausing the queue:

await queue.pauseGroup('tenant-a');
await queue.isGroupPaused('tenant-a'); // true
await queue.resumeGroup('tenant-a');

Inside a processor, apply an immediate cooldown and return the current delivery to waiting with await worker.rateLimitGroup(job, 30_000). This manual deadline works even when the Worker has no group.limit default. The stored setGroupRateLimit override above is different: it customizes a configured fixed-window default and has no effect when all Workers omit that default. The broker installs the manual deadline before moving the active delivery back to waiting. If the lease is stale and that move rejects, the Promise rejects but the group cooldown remains active.

const tenantDepth = await queue.getGroupJobsCount('tenant-a');
const groupedDepth = await queue.getGroupsJobsCount();
const tenantActive = await queue.getGroupActiveCount('tenant-a');
const retryIn = await queue.getGroupRateLimitTtl('tenant-a', 5);
const jobs = await queue.getGroupJobs('tenant-a', 0, 99);
const priorities = await queue.getCountsPerPriorityForGroup('tenant-a');

getGroupJobsCount and getGroupsJobsCount count queued grouped jobs in the waiting, prioritized and delayed states. They exclude active jobs; add getGroupActiveCount when an autoscaler needs both backlog and in-flight work. getGroupRateLimitTtl returns the remaining window in milliseconds, 0 when the optional maxJobs threshold still has room, and -2 when no live window exists.

These are server-authoritative reads. Embedded mode maintains O(1) mirrored depth counters. PostgreSQL queries the durable job/group state, so a read from one broker includes work admitted or claimed through another.

SQLite persists group-specific rate/concurrency configuration across broker restarts, including pause state; its current fixed-window and manual deadline reset with the process, like the existing SQLite queue-level limiter. PostgreSQL persists configuration, pause/manual deadlines, live fixed-window accounting, immutable grouped admission order and round-robin position. Inactive PostgreSQL scheduler rows are reclaimed only after their jobs, overrides and live rate window are gone. queue.obliterateAsync() removes the jobs and all group configuration for that queue.

Rate Limits & ConcurrencyQueue-wide limits and how they compose with group limits
Worker ConcurrencyTotal Worker parallelism and batch pulling
SQLite / PostgreSQLShare group order and budgets across brokers
Job OptionsAll per-job options, including group