Skip to content

Limits and fairness

Flag Default
--max-concurrency 8 queries executing at once
--queue-timeout 5s wait for a slot, then 503
--query-timeout 60s maximum duration, then 504
--max-rows 0 (none) rows per response
--max-concurrency-per-user 0 (none) default per-user quota

With --policy-limits-query data.curral.limits, the policy can return limits for each allowed request:

limits := {"timeout": "30s", "max_rows": 10000} if "analyst" in input.roles
Key
timeout a duration string or seconds
max_rows cuts the response, with the trailer X-Curral-Error: row limit reached
max_concurrency queries this user may run at once
concurrency_group share one quota among several users, e.g. "role:etl"
  • The global flags still apply; the most restrictive value wins.
  • An undefined result means no extra limits.
  • If the rule fails (invalid format, conflicting values), the request is denied with 500. Limits fail closed.
  • The dry run and the audit event show the limits applied.

--max-concurrency is shared by everyone. A per-user quota keeps one user from taking every slot:

limits := {"max_concurrency": 2} if "analyst" in input.roles
limits := {"max_concurrency": 4, "concurrency_group": "role:etl"} if "etl" in input.roles

Over the quota, a request gets 429 immediately (Retry-After: 1) instead of waiting in the queue and holding a global slot.

Measured with --max-concurrency 4, one user firing 12 heavy queries while another runs a simple one:

regular user abusive user
no quota waited 5 s, got 503 8 ran, 4 got 503
--max-concurrency-per-user 2 200 in 2 ms 2 ran, 10 got 429

Throttled requests are audited with decided_by: concurrency and counted in curral_queries_throttled_total.