Skip to content

Rate limiting ​

ProRate limiting needs a JoobQ Pro key. Without one every Core feature keeps working: the Pro routes of the SDK gateway answer 402 pro_required and the console's Pro routes are not served. Get a licence · Core and Pro · Pro features index

rate_limit(key, limit, period_ms) (rateLimit, RateLimit, RateLimitAsync) reserves one slot of a sliding-window limit with POST /api/v1/sdk/rate-limit. The key is yours (a tenant, a partner API, a job class). The check and the reservation are one atomic step.

  • The answer is always 200 {allowed, retry_after_ms} for a valid call, never 429: allowed: true means a slot was granted and consumed; allowed: false gives the milliseconds until one frees up.
  • It does not enqueue, delay or block anything: your code decides what to do with the answer (wait, re-enqueue with delay_ms, skip).
python
@job("conf.ratelimit", queue=QUEUE)
def ratelimit_job(payload, ctx):
    key = "rl-" + payload["probe"]
    r = [ctx.rate_limit(key, 2, 60000) for _ in range(3)]
    report(ctx, "ratelimit", payload, allowed1=r[0].get("allowed"), allowed2=r[1].get("allowed"),
           allowed3=r[2].get("allowed"), retry3=r[2].get("retry_after_ms"))
ts
defineJob<App>({ name: 'conf.ratelimit', queue: QUEUE, perform: async (p, ctx) => {
  const key = `rl-${p.probe}` // limit 2 per 60s: first two allowed, third denied with a retry-after
  const r1 = await ctx.rateLimit(key, 2, 60000)
  const r2 = await ctx.rateLimit(key, 2, 60000)
  const r3 = await ctx.rateLimit(key, 2, 60000)
  return report(ctx, 'ratelimit', p, { allowed1: r1.allowed, allowed2: r2.allowed, allowed3: r3.allowed, retry3: r3.retryAfterMs })
} }),
ruby
class RateLimitJob
  include JoobQ::Job
  job_name "conf.ratelimit"
  queue QUEUE
  def self.perform(p, ctx)
    key = "rl-#{p['probe']}"
    r1 = ctx.rate_limit(key, 2, 60_000)
    r2 = ctx.rate_limit(key, 2, 60_000)
    r3 = ctx.rate_limit(key, 2, 60_000)
    report(ctx, "ratelimit", p, allowed1: r1["allowed"], allowed2: r2["allowed"],
                                allowed3: r3["allowed"], retry3: r3["retry_after_ms"])
  end
end
go
joobq.DefineJob[*App]("conf.ratelimit", opts, func(p map[string]any, ec *EC) (any, error) {
	key := fmt.Sprintf("rl-%v", p["probe"])
	r1, err := ec.Client.RateLimit(ec.Ctx, key, 2, 60000)
	if err != nil {
		return nil, err
	}
	r2, err := ec.Client.RateLimit(ec.Ctx, key, 2, 60000)
	if err != nil {
		return nil, err
	}
	r3, err := ec.Client.RateLimit(ec.Ctx, key, 2, 60000)
	if err != nil {
		return nil, err
	}
	return report(ec, "ratelimit", p, map[string]any{
		"allowed1": r1.Allowed, "allowed2": r2.Allowed, "allowed3": r3.Allowed, "retry3": r3.RetryAfterMs,
	})
}),
rust
JobDef::new("conf.ratelimit", queue.clone(), |p: Value, ctx: &Ctx| {
    let probe = p["probe"].as_str().unwrap_or("");
    let key = format!("rl-{}", probe);
    let client = JoobQClient::new(var("JOOBQ_GATEWAY_URL"), var("JOOBQ_SDK_TOKEN"));
    let r1 = client.rate_limit(&key, 2, 60000)?;
    let r2 = client.rate_limit(&key, 2, 60000)?;
    let r3 = client.rate_limit(&key, 2, 60000)?;
    report(ctx, "ratelimit", &p, json!({"allowed1": r1.allowed, "allowed2": r2.allowed, "allowed3": r3.allowed, "retry3": r3.retry_after_ms}))
}),
java
JobDefinition.<App>of("conf.ratelimit", QUEUE, 3, (p, ctx) -> {
    String key = "rl-" + m(p).get("probe");
    RateLimitResult r1 = ctx.rateLimit(key, 2, 60000);
    RateLimitResult r2 = ctx.rateLimit(key, 2, 60000);
    RateLimitResult r3 = ctx.rateLimit(key, 2, 60000);
    return report(ctx, "ratelimit", p, map("allowed1", r1.allowed(), "allowed2", r2.allowed(),
        "allowed3", r3.allowed(), "retry3", r3.retryAfterMs()));
}),
php
final class RateLimitJob extends ConfJob
{
    public static function jobName(): string { return 'conf.ratelimit'; }
    public static function perform(array $p, ExecutionContext $ctx): mixed
    {
        $key = 'rl-' . $p['probe'];
        $r1 = $ctx->client->rateLimit($key, 2, 60000);
        $r2 = $ctx->client->rateLimit($key, 2, 60000);
        $r3 = $ctx->client->rateLimit($key, 2, 60000);
        return report($ctx, 'ratelimit', $p, [
            'allowed1' => $r1['allowed'] ?? null,
            'allowed2' => $r2['allowed'] ?? null,
            'allowed3' => $r3['allowed'] ?? null,
            'retry3' => $r3['retry_after_ms'] ?? null,
        ]);
    }
}
csharp
JobDefinition<App>.Define("conf.ratelimit", Queue, 3, async (p, ctx) =>
{
    var probe = p?["probe"]?.GetValue<string>();
    var key = "rl-" + probe;
    var r1 = await ctx.RateLimitAsync(key, 2, 60000);
    var r2 = await ctx.RateLimitAsync(key, 2, 60000);
    var r3 = await ctx.RateLimitAsync(key, 2, 60000);
    return await Report(ctx, "ratelimit", p, new()
    {
        ["allowed1"] = r1.Allowed, ["allowed2"] = r2.Allowed, ["allowed3"] = r3.Allowed, ["retry3"] = r3.RetryAfterMs,
    });
}),
Not in every language
  • Crystal: Gateway-mediated Pro rate limiter: Crystal calls JoobQPro::RateLimiter directly.

Tested against a real server in every language shown.

The worker above reserves three slots of a limit of 2 per 60 seconds: the first two are allowed, the third is denied with a positive retry_after_ms.

Options ​

FieldRange
key1 to 200 characters
limit1 to 10,000,000
period_ms1 to 86,400,000 (a day)

A value out of range answers 422 invalid_request.

Without a Pro key ​

Every call answers 402 pro_required. The check is made on every call, so it starts answering 402 as soon as the key expires.

Crystal ​

Crystal code calls JoobQPro::RateLimiter.reserve directly (it checks the licence on every call); JoobQ Pro also has a rate-limit middleware for Crystal jobs.

JoobQ Core is MIT licensed. JoobQ Pro needs a licence key.