Per-principal query memory limits
QuestDB Enterprise can limit the native memory tracked for a single query, overriding the server-wide query memory limit for a specific user, group, or service account. Use it to help prevent one tenant's runaway query from exhausting shared memory, or to grant a trusted principal more headroom than the default. Per-principal limits are available since QuestDB Enterprise 4.0.2; the server-wide workload limits they override are available since QuestDB 10.0.0.
Set a limit with ALTER USER,
ALTER GROUP, or
ALTER SERVICE ACCOUNT:
ALTER USER john SET MEMORY LIMIT 512M;
ALTER GROUP analysts SET MEMORY LIMIT 2G;
ALTER SERVICE ACCOUNT ingest_app SET MEMORY LIMIT 1G;
ALTER USER john SET MEMORY LIMIT UNLIMITED; -- clear the user's own limit
The value is a byte count or a size with a K, M, or G suffix. Each suffix
multiplies by 1024, so 512M is 536870912 bytes. Setting a limit requires the
SET MEMORY LIMIT permission, which is included in GRANT ALL and held
implicitly by database admins.
Treat SET MEMORY LIMIT as an administrative permission, not a self-service
one. It takes no entity name, so its holder can set the limit of any principal,
including its own. Because a per-principal limit overrides the workload limit
rather than tightening it (see How limits resolve), a
non-admin who holds it can raise its own ceiling above
cairo.query.memory.limit.bytes. The built-in admin is exempt only as a
target: its limit cannot be set at all.
GRANT ALL expands to individual permissions at the moment it is granted. A
principal granted ALL before upgrading to a version with this feature does
not acquire SET MEMORY LIMIT; only grants issued after the upgrade include it,
and no migration backfills it. Grant it explicitly to existing administrators:
GRANT SET MEMORY LIMIT TO admins;.
How limits resolve
QuestDB resolves the limit for a query by strict precedence. It takes the first level that is set, not the smallest across levels:
- The principal's own limit. For a user this is the user's own limit; for a query that assumes a service account it is the service account's limit.
- A group limit (users only). When a user has no limit of its own, it inherits the most restrictive (smallest positive) limit among the groups it belongs to. Service accounts never inherit group limits.
- The workload limit. Otherwise the server-wide
cairo.query.memory.limit.bytesapplies.
A value of 0 (or UNLIMITED) means "not set" at that level, so resolution
falls through to the next one. A more specific level, when set, fully overrides
the broader one and binds even when it is larger, so a per-user or per-group
override can raise a principal's ceiling above the workload limit, not only lower
it. A user who assumes a service account takes on the service account's limit.
The built-in admin cannot be given a limit, so ALTER USER admin SET MEMORY LIMIT is rejected with Cannot set memory limit for built-in admin, and the
admin runs under the workload limit. Size that limit with the admin's
diagnostic queries in mind.
The cap applies to the principal's queries on both the primary and replicas. The
statement itself runs on the primary only: a replica rejects it with
replica cannot set memory limit and receives the new value through the
replicated ACL tables. A changed limit applies to the principal's next query,
including on connections that are already open, while a query already running
keeps the limit it started with.
A query that crosses its limit fails with the same
query memory limit exceeded [workload=QUERY, ...] error as a breach of the
workload limit. See
memory limits for the message
format.
A limit bounds a single query. Two concurrent queries by the same principal each run under the full limit, so the principal's aggregate usage can exceed it. The cap guards against one runaway workload, not total concurrent usage.
What a per-principal limit covers
Per-principal limits have the same coverage as workload limits. They apply to tracked native allocations for:
- The principal's queries.
- Its background
COPY ... TOexports, which use the issuing principal's limit. Some memory used to produce the export file is not yet covered, and exports do not appear inquery_activity, so their usage cannot be observed while they run. UPDATEon a non-WAL table, which is applied on the caller's own thread and acquires its own query-workload tracker.
It does not reach work that runs under an internal context rather than a principal. That work stays bounded only by its own workload limit:
UPDATEon a WAL table, the default table type, because the statement is applied by the WAL apply job and draws on that job'scairo.wal.apply.memory.limit.bytesbudget instead. Whether a largeUPDATEis capped by aSET MEMORY LIMIToverride therefore depends on the table type. A WAL-tableUPDATEthat breaches the WAL apply limit suspends the table for every principal untilALTER TABLE RESUME WAL, so the failure is not isolated to the tenant that issued it.- Materialized view refresh, live view refresh, and WAL apply itself. A WAL apply batches many principals' transactions into one tracker, so it could not attribute usage to a single principal in any case.
COPY ... FROMimports, which acquire no memory tracker at all and are unaffected by either kind of limit.
An external (SSO/OIDC) user can only receive a limit by inheriting one from a
mapped group: ALTER USER ... SET MEMORY LIMIT on an external user is rejected
with Cannot set memory limit for external user. The user receives its
inherited group limit at login. On the primary, a later
ALTER GROUP ... SET MEMORY LIMIT also reaches the user's open sessions at
their next query. On a replica, and on the primary after a restart or promotion,
an existing external session keeps the limit it logged in with until it
reconnects. See Mapping groups and permissions
for the external alias flow.
Inspecting limits
SHOW USERS,SHOW GROUPS, andSHOW SERVICE ACCOUNTSreport per-principal limits in amemory_limitcolumn, in bytes. For users, it is the user's own limit or, when it has none, the most restrictive of its groups'. For groups and service accounts, it is the entity's own limit. The column excludes the server-wide workload limit.nullmeans the entity has no limit of its own and, for a user, no inherited one either, so the workload limit applies to its queries.- The filtered forms
SHOW GROUPS userNameandSHOW SERVICE ACCOUNTS { userName | groupName }carry the column too, reporting each listed group's or service account's own limit. This shows which inherited limit binds for a user with no limit of its own. query_activityexposes the effective limit and live usage of each running query through itsmemory_limitandmemory_usedcolumns.SHOW USERdoes not carry the column. The unfilteredSHOW USERS,SHOW GROUPS, andSHOW SERVICE ACCOUNTSrequireLIST USERS; the filtered forms requireUSER DETAILSunless the caller names itself or one of its own groups. A user withoutLIST USERScan still read its own effective cap from thememory_limitcolumn ofquery_activity, which always lists the caller's own queries.- The stored value is persisted on the
sys.acl_entitiessystem table. That table is protected: only the built-in admin can read it, and an ACL principal holdingDATABASE ADMINis still denied.
Upgrading
memory_limit is appended as the last column of SHOW USERS, SHOW GROUPS,
and SHOW SERVICE ACCOUNTS, including their filtered forms, whether or not any
limit is set. SELECT * on sys.acl_entities returns one more column as well.
Clients that read these results by position must be updated; clients that read
by column name are unaffected.
The SHOW results carry the column from the first start of the upgraded
binary, reading null until limits are set. The sys.acl_entities column that
stores the value is added by an automatic migration when an upgraded node first
starts as a primary or is promoted from replica to primary. Persisted principal
limits are not enforced until the migration has been applied. The migration passes
through two windows, and each refuses a different set of statements:
-
Before the column exists, a
SET MEMORY LIMITwith a non-zero size is refused with:Cannot modify ACL entities: the memory_limit column has not been migrated in yet; retry shortly, or restart the node if it persistsSET MEMORY LIMIT UNLIMITEDandALTER ... ENABLEorDISABLEstill succeed. -
Once the column has been added but WAL apply has not yet reached it,
SET MEMORY LIMIT UNLIMITEDandALTER ... ENABLEorDISABLEare refused as well, so that a stale in-memory value cannot overwrite a stored limit. A non-zeroSET MEMORY LIMITkeeps the first message; the others read:Cannot set memory limit while the ACL memory_limit column migration is still being applied, retry [name=john]or
Cannot enable while ...andCannot disable while ...for a status change.
The window normally closes on its own once WAL apply catches up, so retry the statement first. If the error persists, restart the node: the migration runs again at startup.