Rate limiting
402 pro_required and the console's Pro routes are not served. Get a licence · Core and Pro · Pro features indexrate_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: truemeans a slot was granted and consumed;allowed: falsegives 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).
@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"))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 })
} }),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
endjoobq.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,
})
}),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}))
}),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()));
}),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,
]);
}
}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::RateLimiterdirectly.
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
| Field | Range |
|---|---|
key | 1 to 200 characters |
limit | 1 to 10,000,000 |
period_ms | 1 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.