Fair queuing for multi-tenant job queues: an interactive simulator
When many tenants share one pool of workers, a single first-in, first-out queue lets whoever submits the most work take everything: one customer’s bulk import delays every other customer’s jobs for minutes. Fair queuing keeps a queue per tenant and serves them in turn, so each tenant with work waiting gets an equal share of the workers, and a tenant alone gets them all.
The playground shares 8 workers between three tenants. Pick a scenario, compare the schedulers in the table, and click one to see who held the workers and how long jobs waited.
8 workers, each finishing a job a second, shared by tenants A, B and C. Tenant A drops 600 jobs at once at 10 s; B and C each send 2 jobs a second throughout. With one FIFO queue, B's and C's jobs wait behind all of A's: up to 74 seconds for jobs that take one. Fair queuing gives each tenant with work an equal turn, so B and C wait nothing and A gets every worker they leave free, finishing at 160 s instead of 85 s. A cap of 4 running jobs per tenant does as well here.
FIFO, caps and fair queuing
When tenant A drops 600 jobs at once, a FIFO queue runs them before anything B and C submit afterwards, so their one-second jobs wait up to 74 seconds. Fair queuing serves the three in turn: B and C are served almost at once, and A still gets every worker they do not use, finishing at 160 seconds instead of 85. The total work done is the same; only who waits changes.
A per-tenant concurrency cap looks similar and is simpler, but it has two weaknesses. It is not work-conserving: when A is alone, the cap still holds it to 4 workers while 4 sit idle, doubling its finish time. And it does not protect anyone when several tenants are at their cap at once: with A and B both flooding, their caps add up to all 8 workers and C waits up to 74 seconds anyway.
- Weights. Deficit or weighted round robin gives paying tiers a bigger share without starving anyone.
- Job sizes. Fairness by job count favours tenants with big jobs; schedulers such as DRR charge each tenant for the time or bytes it used instead.
- Many tenants. A queue per tenant is cheap in memory, but a scheduler scanning thousands of queues needs an active list of tenants that have work, as in the code below.
- Where it applies. Job runners, API gateways, database connection pools, network links: Linux’s fq_codel and Kafka’s quotas both rest on the same idea.
A fair queue in code
from collections import deque
class FairQueue:
"""One queue per tenant, served round robin; deficit round robin adds weights."""
def __init__(self, weights=None):
self.queues, self.order = {}, deque()
self.weights, self.credit = weights or {}, {}
def put(self, tenant, job):
if tenant not in self.queues:
self.queues[tenant] = deque()
self.order.append(tenant)
self.credit[tenant] = 0
self.queues[tenant].append(job)
def get(self):
while self.order:
tenant = self.order[0]
if not self.queues[tenant]: # nothing waiting: drop out of the rotation
self.order.popleft(); del self.queues[tenant]; continue
if self.credit[tenant] <= 0: # turn used up: top up, go to the back
self.credit[tenant] += self.weights.get(tenant, 1)
self.order.rotate(-1); continue
self.credit[tenant] -= 1
return self.queues[tenant].popleft()
return NoneIn a system design interview
Mention it whenever a shared queue serves many customers: “Jobs go into a queue per tenant, and workers pull from tenants round robin, weighted by plan, so one tenant’s backfill of a million jobs cannot delay anyone else’s. A tenant alone still gets the whole fleet. Per-tenant rate limits sit at the API, and a tenant’s backlog size is a metric we alert on.” Expect the follow-ups: priorities within a tenant, fairness across a distributed set of workers, and what happens when one tenant’s jobs are much longer than others’.
Read the guide to multi-tenancy or background jobs, see shuffle sharding and bulkheads for isolation, or watch a strong candidate design a job scheduler.
Questions people ask
What is fair queuing?
A scheduling approach that keeps a separate queue per tenant or flow and serves the queues in turn, so every tenant with work waiting gets an equal share of capacity, regardless of how much work any one of them submitted.
Why is a single FIFO queue unfair in a multi-tenant system?
Because a tenant that submits a large batch fills the queue, and everything other tenants submit afterwards waits behind the whole batch. Short jobs from small tenants can wait minutes even though the system as a whole has plenty of capacity over time.
What does work-conserving mean?
A scheduler is work-conserving if it never leaves capacity idle while there is work waiting. Fair queuing is: a tenant alone gets every worker. A fixed per-tenant concurrency cap is not, because it holds a lone tenant to its cap while other workers sit idle.
Are per-tenant concurrency limits enough?
They stop a single tenant from taking everything, but they waste capacity when other tenants are idle, and they do not protect anyone when several tenants hit their limits at once, since the limits can add up to the whole pool. Fair queuing handles both cases.
What is weighted or deficit round robin?
Variants of fair queuing. Weighted round robin gives tenants turns in proportion to a weight, for example by plan. Deficit round robin gives each queue a credit per round and charges it for the actual size of the work, so tenants with large jobs do not get more than their share.
How do you implement fair queuing across many workers?
Keep jobs in per-tenant queues, for example Redis lists or a table indexed by tenant, plus a list of tenants that currently have work. Workers pick the next tenant from that list in rotation and take one job from it, which keeps the cost independent of the number of idle tenants.
Where is fair queuing used?
In job schedulers and task queues shared by customers, in API gateways and database connection pools, and in networking, where algorithms such as fq_codel share a link fairly among flows. Cluster schedulers like YARN’s fair scheduler apply the same idea to compute.