Fork me on GitHub

This page explains how RQ’s rate limiting works under the hood and why it is built the way it is. It’s aimed at contributors; see the user guide for the public API and supported behavior.

The strategy implemented today is concurrency-based: it caps how many jobs sharing a key can be queued or executing at the same time, across all workers and queues using the same Redis database.

Terminology

The Mental Model

A step-by-step example — three jobs sharing a rate limit that allows two at a time:

rate_limit = RateLimit(key='reports', concurrency=2)

job_a = queue.enqueue(generate_report, rate_limit=rate_limit)  # slot free → queued
job_b = queue.enqueue(generate_report, rate_limit=rate_limit)  # slot free → queued
job_c = queue.enqueue(generate_report, rate_limit=rate_limit)  # no slots left → rate_limited

Contents of the queue and the rate limit registry (its allowed and rate_limited sets) after each event:

Event Queue Allowed Rate Limited
enqueue job_a job_a job_a  
enqueue job_b job_a, job_b job_a, job_b  
enqueue job_c job_a, job_b job_a, job_b job_c
job_a finishes job_b, job_c job_b, job_c  

The key subtlety: a rate_limited job exists only as a job hash plus an entry in the rate_limited sorted set. It is not on any queue — workers cannot see it until promotion pushes it onto its origin queue.

Enqueueing always goes through the rate limit registry: every rate-limited job is saved as rate_limited and added to the waiting set first, then promotion runs — when capacity is free, the job promoted is usually the one just parked. This single admission path keeps waiting jobs FIFO (nothing jumps ahead of existing waiters), puts the capacity decision in exactly one place and makes plain enqueue(), scheduled jobs and resolved dependents behave identically.

Data Model in Redis

Everything lives in rq/rate_limit.py (RateLimit, RateLimitRegistry). Each rate limit key has one registry, stored as:

Jobs persist rate_limit_key and rate_limit_concurrency on their hash; Job.has_rate_limit is true when both are set.

The Two Operations and Why They’re Lua

Each operation runs as a single Lua script, so the capacity check and the promotion execute atomically in Redis and cannot interleave across concurrent workers.

Interactions Worth Knowing

Rate Limit Registry Cleanup

Rate limit state can go stale: a worker can crash after taking a slot and before releasing it, and deferred promotions leave freed capacity while jobs are still waiting. RateLimitRegistry.cleanup() reconciles this. It runs as part of clean_registries, the periodic registry maintenance performed by workers, and:

  1. Releases stale allowed entries and attempts to promote a waiter after each release.
  2. Attempts another promotion in case capacity was freed elsewhere.
  3. Deletes the registry once both sets are empty.

Known Sharp Edges