Sidekiq Ent: Rate Limiting

Work In Progress

Official doc: https://github.com/sidekiq/sidekiq/wiki/Ent-Rate-Limiting

Limiter is using Sidekiq’s Redis. If you use it too much, you’ll see high CPU utilization from your Redis. Redis is single threaded so bigger instance which means more vCPU and memory does not really help. Limiter makes sure running jobs finish before the enqueue new jobs.

Usage: Limit calls to 3rd-party API. Can use everywhere, not just Sidekiq Jobs.

Sidekiq reschedule overlimit job with linear backoff (try again in 5mins, try again in 10mins, try again in 15mins) up to 25 times (~ 3 weeks). Then mark the failed job as retry.

Sidekiq::Limiter instance has a within_limit method, you can apply limit to the block of code.

Note limiter is not throttling. For throttling, use something like Sidekiq::Throttled. With Sidekiq::Throttled which you also get limiter feature.

The default backoff logic is

(300 * job["overrated"]) + rand(300) + 1

where job["overrated"] starts from 0, 1, ..., 24 (25 times).

You can configure your limiter backoff to reschedule 10 times with expotential retry of 2 seconds plus some jitter like so:

Sidekiq::Limiter.concurrent(
  "CustomBackoff",
  reschedule: 10,
  backoff: ->(limiter, job, exception) do
    (2**job["overrated"]) + rand(5)
  end
)

This is useful for conditional limiter:

limiter = if should_rate_limit?
  Sidekiq::Limiter.leaky(org_uniq_id, 60, :minute, wait_timeout: 0)
else
  Sidekiq::Limiter.unlimited
end

...

limiter.within_limit do
  ...
end

Limit to 1 op at a time and wait 10s to get lock:

limiter = Sidekiq::Limiter.concurrent("SingleTransaction", 1, wait_timeout: 10)
limiter.within_limit do
  # work...
end

Above will ensure the block only being run by one exclusively. Can think it is an execution Mutex.

Each time interval is a bucket. Limit how many operations can do within the bucket. Reset in the next bucket.

E.g. Limit up to 600 calls each hour.

[0900-1000 bucket - Remaining: 600]
09:00 — 100 calls
09:01 — 100 calls
09:02 — 100 calls
09:03 — 100 calls
09:04 — 100 calls
09:05 — 100 calls
09:06 — Cant make calls
...
09:59 — Cant make calls

[1000-1100 bucket - Remaining: 600]
# 250 requests per minute — 15,000 per hour.
def github_api_call
  github_api_limiter = Sidekiq::Limiter.bucket("github-api-for-#{organization_id}", 250, :minute, wait_timeout: 5)
  github_api_limiter.within_limit do
    # github api call
  end
rescue Sidekiq::Limiter::OverLimit

end

v2.2 added leaky bucket.

Implementation Story

Sidekiq::Limiter.leaky("key", 600, :hour) # 3600s, 6s 1op

Each time interval is a bucket. Limit how many operations can do within the bucket. After reached the limit, you get extra operation every 6 seconds: "drip". After one hour, bucket will be reset.

E.g. Limit up to 600 calls each hour. +1 every 6 second.

[0900-1000 bucket - Remaining: 600]
09:00 — 100 calls
09:01 — 100 calls
09:02 — 100 calls
09:03 — 100 calls
09:04 — 100 calls
09:05 — 100 calls

-- consume all in a burst --

[0900-1000 bucket - Remaining: 0]
09:06

[0900-1000 bucket - Remaining: 1]
09:06:06

[0900-1000 bucket - Remaining: 2]
09:06:12

[1000-1100 bucket - Remaining: 600]

Sidekiq::Limiter.window("key", 600, :hour)

Each time interval is a sliding window. Limit how many operations can do within the sliding window. Most Strict.

E.g. Limit up to 600 calls every hour. +1 call every 2 minutes.

[0900-1000 bucket - Remaining: 400]
09:00 — 100 calls

[0901-1001 bucket - Remaining: 300]
09:01 — 100 calls

[0902-1002 bucket - Remaining: 202]
09:02 — 100 calls

[0903-1003 bucket - Remaining: 102]
09:03 — 100 calls

[0904-1004 bucket - Remaining: 43]
09:04 — 100 calls

[0905-1005 bucket - Remaining: 0]
09:05 — 43 calls

[0906-1006 bucket - Remaining: 1]
[0907-1007 bucket - Remaining: 1]
[0908-1008 bucket - Remaining: 2]
...
[1008-1108 bucket - Remaining: 600]