Limits and fairness
Global limits
Section titled “Global limits”| 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 |
Per-role limits
Section titled “Per-role limits”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.
Fairness between users
Section titled “Fairness between users”--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.roleslimits := {"max_concurrency": 4, "concurrency_group": "role:etl"} if "etl" in input.rolesOver 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.