Rate Limiting
Token bucket rate limits per task — count/period syntax, persistence, retries.
Token bucket rate limits per task — count/period syntax, persistence, retries.
flexiq uses a token bucket (a standard rate-limit algorithm that refills allowance over time) algorithm to limit how fast tasks execute. Rate limits are per-task and persisted in the queue database (SQLite or Postgres).
@queue.task(rate_limit="100/m") # 100 per minute
def send_email(to, subject, body):
...
@queue.task(rate_limit="10/s") # 10 per second
def api_call(endpoint):
...
@queue.task(rate_limit="3600/h") # 3600 per hour
def generate_report(report_id):
...Rate limits use the format count/period:
| Format | Meaning |
|---|---|
"10/s" | 10 per second |
"100/m" | 100 per minute |
"3600/h" | 3600 per hour |
The token bucket algorithm:
max_tokens = count and a refill_rate = count / periodToken bucket state (current tokens, last refill time) is stored in the queue database (SQLite or Postgres). This means rate limits survive worker restarts.
Deferral is the right default: the job keeps its place and runs once tokens are
available. But some work is worth less than the backlog it would build — a
metrics sample, a cache warm — and for that, on_excess="drop" sheds the job
instead of rescheduling it:
@queue.task(rate_limit="10/s", on_excess="drop")
def record_sample(metric, value):
...A dropped job is dead-lettered on the spot, not silently deleted. Its
reason is prefixed rate_limit: and its DLQ metadata is {"shed":"rate_limit"},
so shedding stays visible in the dashboard and countable in metrics — an
operator can always tell shedding apart from data loss. The DLQ auto-retry
sweep skips these entries: resurrecting a job the scheduler deliberately shed
would undo the shed.
Three things worth knowing:
on_excess applies to the limit on this task and to the limit on the
queue it runs in — either one rejecting means the same thing to the caller.Flow control walks through what a shed job looks like in the dashboard, and where throttling ends and debouncing begins.
Rate limits apply to the task name, regardless of which queue the job is in:
@queue.task(rate_limit="10/s", queue="emails")
def send_email(to, subject, body):
...
# Both of these are rate-limited together (same task name)
send_email.delay("alice@example.com", "Hi", "Body")
send_email.apply_async(args=("bob@example.com", "Hi", "Body"), queue="urgent")Coming from Celery: Celery's
rate_limitis enforced per worker — the effective rate scales with worker count. flexiq enforces it globally per task name at the scheduler, so"100/m"means 100/min across the whole deployment regardless of how many workers run.
Rate limiting and retries work together seamlessly. If a rate-limited task fails and retries, the retry attempt is also subject to the rate limit:
@queue.task(rate_limit="5/s", max_retries=3, retry_backoff=2.0)
def external_api(url):
response = requests.get(url)
response.raise_for_status()
return response.json()