Governor Limits
Query Studio enforces per-user, per-tenant, and global limits on every Workbench query path (Quick + Deep + Expert + scheduled). The WorkbenchGovernorService reads each knob from the standard Billerang provider.application settings, which means all values are hot-reloadable without restarting WildFly.
Knob reference
| Setting key | Default | Purpose |
|---|---|---|
workbench.governor.enabled | true | Master switch. Set to false to short-circuit all checks (NOT recommended in production). |
workbench.governor.maxConcurrentSyncPerUser | 3 | In-flight sync Quick / Deep queries per user. |
workbench.governor.maxConcurrentAsyncPerUser | 3 | In-flight async Quick / Deep / Expert queries per user. |
workbench.governor.maxQueriesPerMinutePerUser | 30 | Sliding-window rate, per-node in-memory. |
workbench.governor.maxQueryDurationSyncSec | 30 | Sync statement_timeout. |
workbench.governor.maxQueryDurationAsyncSec | 600 | Async job timeout. |
workbench.governor.maxRowsSync | 1000 | Sync row hard cap. Quick + Deep results are silently truncated to this. |
workbench.governor.maxRowsAsync | 1000000 | Async row hard cap. Forces LIMIT for Expert if the user requests more. |
workbench.exports.retentionDays | 30 | Retention for files under {providerRoot}/exports/queryStudio/. |
workbench.exports.cleanupEnabled | true | Master switch on the nightly retention sweep. |
workbench.templates.configDir | (unset) | Optional override directory for YAML system templates. |
V2 candidates (not enforced today): per-tenant + global concurrency caps, Expert-specific concurrency cap, and per-day rate limit. Reliable enforcement across a multi-node cluster requires Redis / Infinispan for the rate counters and an explicit tenant column on
query_execution_resultfor the concurrency queries. Both are tracked as V2 backlog. The per-user knobs above ARE enforced today and are sufficient for single-tenant deployments.
How concurrency is computed
Concurrency is shared-DB correct: the count comes from a single SQL query against query_execution_result filtered on endDate IS NULL and auditable.creator = :user (or tenant). This means a user who hits their per-user cap on node A can't sneak past it on node B — the count is global.
SELECT COUNT(qer)
FROM QueryExecutionResult qer
WHERE qer.endDate IS NULL
AND qer.auditable.creator = :user
How rate is computed
Rate is per-node in-memory for simplicity. The bucket counter resets every 60 s for per-minute and every 24 h for per-day. In a multi-node cluster the per-tenant rate may diverge slightly across nodes — this is an accepted soft-cap behaviour.
For a strict global rate, run the rate counter through Redis or Infinispan; that's a V2 change and is gated on someone needing it.
What happens when a limit hits
The governor throws GovernorViolationException. The resource layer maps it to:
- HTTP
429 Too Many Requests Retry-After: <seconds>header- Body:
{
"code": "GOVERNOR_LIMIT_EXCEEDED",
"limit": "maxConcurrentAsyncPerUser",
"current": 3,
"max": 3,
"retryAfterSeconds": 30,
"message": "Async query concurrency limit reached (current=3, max=3)"
}
The frontend's GovernorViolationToast reads this body and shows an amber toast with the specific limit, current / max counts, and the retry-after countdown. It auto-dismisses after the retry-after window or on click.
Tuning guidance
Single-tenant deployment
The defaults are conservative for a single-tenant deployment with a few power users:
- Bump
maxConcurrentSyncPerUserto10if the team works fast and the DB has headroom. - Bump
maxRowsSyncto5000if exports under 5 k rows are routine. - Drop
maxQueriesPerMinutePerUserto60if you see legitimate "too many queries" 429s.
Multi-tenant SaaS
The per-user knobs are not sufficient for multi-tenant clusters because a single tenant with many users could starve the cluster. Multi-tenant guardrails (per-tenant / global / Expert-specific concurrency, per-day rate) are V2 backlog. Until they ship, the recommendation for multi-tenant deployments is:
- Withhold the
queryExpertrole entirely (the trust model covers why). - Tighten
maxConcurrentSyncPerUserandmaxConcurrentAsyncPerUserproportional to your DB connection pool — start with1per user if you have many tenants. - Lower
maxRowsSyncandmaxRowsAsyncto constrain blast radius. - Lower
maxQueryDurationSyncSecandmaxQueryDurationAsyncSecso any one query frees its connection sooner.
Cluster with read-replicas
If you provision a read-only replica via the optional MeveoAdminReadOnly persistence unit (see WorkbenchEntityManagerProducer.java), Quick + Deep + Expert reads route there automatically. Then:
- The sync timeout can be longer (
maxQueryDurationSyncSec=60) — the replica isn't on the user-facing transaction path. - The async timeout can be much longer (
maxQueryDurationAsyncSec=3600) — long analytical queries no longer compete with billing transactions.
Per-tenant override
Standard Billerang settings support per-provider override. To override maxRowsSync for tenant pluxee_be only, set pluxee_be.workbench.governor.maxRowsSync=5000 in the per-provider settings store.
Disabling the governor
Setting workbench.governor.enabled=false short-circuits all assertCanRun* calls. Do not do this in production — there's no upper bound on the database load Query Studio can generate. The flag exists for diagnostic purposes during incident response only.