When multiple tenants share a background job queue, greedy tenants can monopolize workers and degrade quality of service for everyone else. This deep-dive examines four strategies for achieving fair scheduling in Ruby: shuffle-sharding (workload isolation via overlapping shard subsets), interruptible iteration (time-bounded resumable jobs using Shopify's job-iteration gem), throttling (leaky-bucket detection routing excess jobs to a slow queue), and per-tenant queues with a custom scheduler (a planner that promotes jobs from per-tenant virtual queues into the main queue based on a fairness policy). Each of Sidekiq, Sidekiq Pro, Solid Queue, and GoodJob is analyzed for dynamic queue support and weighted polling. The author recommends starting with the custom scheduler approach for its flexibility, falling back to the other strategies based on workload shape and scale.
Table of contents
Fair by design: orchestrating background jobs in RubyLatency isn’t the whole storyWhat does “fair” mean?A look inside background job processorsFairness strategies“Fair” multi-tenant prioritization of Sidekiq jobs—and our gem for it!Choosing a fairness strategyQuestions this post answers
How does Sidekiq handle dynamic per-tenant queues for fair scheduling?
Sidekiq open source uses a single BRPOP call across a static, fixed queue list — you cannot add or remove queues while the process is running. This makes true dynamic per-tenant queues impossible without a workaround. Sidekiq Pro replaces BRPOP with sequential LMOVE calls for reliability, which scales even worse with large queue counts and also lacks dynamic queue support. Teams building multi-tenant Ruby apps track Sidekiq limitations and workarounds like these on daily.dev.
What is the difference between shuffle-sharding and regular sharding for background job fairness?
Regular sharding assigns each tenant to exactly one shard, so a greedy tenant blocks all co-tenants in that shard. Shuffle-sharding assigns each tenant a small overlapping subset of shards, so the chance that one tenant fully blocks another drops combinatorially — from 10% with one shard to roughly 2% with two overlapping shards out of ten — while improving resource utilization because workers cover multiple shards. Developers weighing queue isolation trade-offs for their SaaS architecture find comparisons like this on daily.dev.
How does the job-iteration gem help with fair background job scheduling in Ruby?
Shopify's job-iteration gem lets you set a maximum runtime per job execution via `job_iteration_max_job_runtime`. Once the time budget expires, the job saves its cursor and re-enqueues itself, freeing the worker for other tenants. This turns long monolithic jobs into time-bounded resumable iterations, preventing any single tenant's batch from holding a worker indefinitely. Ruby engineers implementing resumable or fair-scheduled jobs keep up with the job-iteration ecosystem on daily.dev.