Skip to content

Document per-query and per-principal memory limits - #462

Open
puzpuzpuz wants to merge 18 commits into
mainfrom
puzpuzpuz_per_query_mem_limit
Open

Document per-query and per-principal memory limits#462
puzpuzpuz wants to merge 18 commits into
mainfrom
puzpuzpuz_per_query_mem_limit

Conversation

@puzpuzpuz

@puzpuzpuz puzpuzpuz commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Documents the new memory-limit feature across configuration, monitoring, and access control.

OSS — per-workload memory limits

  • configuration/cairo-engine.md: new "Memory limits" section covering the reloadable workload limits (cairo.query.memory.limit.bytes, cairo.mat.view.refresh.memory.limit.bytes, cairo.wal.apply.memory.limit.bytes, plus a pointer to cairo.live.view.refresh.memory.limit.bytes, which the live-views config page already documents), per-workload breach behavior, and a note that memory-mapped memory does not count toward a limit. Cross-links added from the WAL, materialized-views, and capacity-planning pages.
  • query/functions/meta.md: query_activity gains the memory_used and memory_limit columns.
  • configuration/materialized-views.md: documents cairo.mat.view.refresh.busy.retry.limit and cairo.mat.view.refresh.busy.retry.timeout, which govern what happens after a refresh breaches its memory limit and were previously undocumented. The breach description in cairo-engine.md links to them.

Enterprise — per-principal memory limits

  • query/sql/acl/alter-user.md, alter-service-account.md, and a new alter-group.md (+ sidebar): SET MEMORY LIMIT { <size> | UNLIMITED }.
  • security/rbac.md: new "Memory limits" section — how limits resolve, what a per-principal limit does and does not cover, how to inspect limits, the SET MEMORY LIMIT permission, and ALTER GROUP in the command list.
  • query/sql/show.md: the memory_limit column on SHOW USERS, SHOW GROUPS and SHOW GROUPS <user>, and SHOW SERVICE ACCOUNTS and SHOW SERVICE ACCOUNTS <user | group>.

Limit resolution — override, not min. Limits resolve by strict precedence: a principal's own limit → (users only) the most-restrictive limit among its groups → the global cairo.query.memory.limit.bytes workload limit. A 0/UNLIMITED value at a level means "not set" and falls through to the next. A more specific limit, when set, fully overrides the broader one and binds even when larger, so a per-user or per-group override can raise a principal's ceiling above the workload limit, not only lower it. Service accounts never inherit group limits. (Earlier drafts of these docs described a most-restrictive "min" behavior; revised per questdb/questdb-enterprise@fb4e050 "Make per-entity memory limits override, not min".)

Corrections made after checking against the implementation

Two statements in earlier revisions of this PR were wrong, and both were verified against the Enterprise code rather than re-reasoned:

  • UPDATE was described as never picking up a per-principal limit. False for a non-WAL table. UpdateOperation.apply() passes the caller's own SqlExecutionContext to UpdateOperatorImpl.executeUpdate, which calls queryRegistry.register(...), which acquires a QUERY-workload tracker from that context's security context — so a non-WAL UPDATE is capped by SET MEMORY LIMIT. An operator sizing one from the old text would have met an unexpected OOM abort. Only the WAL path (the default table type) runs under the WAL apply job's tracker. Replaced with a "What a per-principal limit covers" section that splits the two.
  • SHOW GROUPS <user> and SHOW SERVICE ACCOUNTS <user | group> were shown without a memory_limit column, with prose saying so explicitly. They carry it — AbstractShowLinkedEntitiesCursorFactory adds it to both metadata shapes, pinned by ShowAclTest. Since these are among the results that break a positional client, the omission hid half the breaking change.

Example tables that disagreed with each other on the same entity's limit were also made consistent.

Behaviour the docs previously did not mention at all

  • COPY ... TO exports are capped by the issuing principal's limit. The export runs under that principal's own context on the query workload, so a limit must be sized for the largest single thing the principal runs, exports included — not only for its interactive queries.
  • SET MEMORY LIMIT is self-escalating. It takes no entity name, so its holder can set any principal's limit including its own; combined with override-not-min, a non-admin holding it can raise its own ceiling above cairo.query.memory.limit.bytes. Documented as a warning, together with the GRANT ALL expansion — a principal granted ALL before the upgrade does not acquire the new permission.
  • The breaking change. Five results gain a column on upgrade (SHOW USERS, SHOW GROUPS and SHOW GROUPS <user>, SHOW SERVICE ACCOUNTS and SHOW SERVICE ACCOUNTS <user | group>), plus SELECT * on sys.acl_entities. Positional clients must be updated; name-based clients are unaffected.
  • sys.acl_entities stores the value, and is readable only by the built-in admin — a principal holding DATABASE ADMIN is still denied.
  • memory_limit means different things per statement: the effective limit in SHOW USERS, the entity's own limit in SHOW GROUPS / SHOW SERVICE ACCOUNTS, since neither inherits one.
  • An external (SSO/OIDC) user's inherited group limit refreshes at next login, not on its current session — the same refresh-on-login model that already governs group-granted permissions for external users.

Re-validation against the merged code

Both source PRs merged after the last revision of this doc PR (OSS on 2026-06-20, Enterprise on 2026-09-08), and OSS master moved on afterwards. Every claim was re-checked against merged Enterprise main and OSS master (questdb submodule pin fef82615), and these were stale:

  • There are four workload limits, not three. Live views (feat(sql): add live views questdb#7461) added LIVE_VIEW_REFRESH as a workload with its own reloadable cairo.live.view.refresh.memory.limit.bytes. The live-views config page on main already documents the key, so the memory-limits section now points to it rather than duplicating it, and the intro, capacity-planning blurb, and the RBAC "not covered by a principal limit" list name live view refresh.
  • A mat view refresh breach no longer invalidates the view on first breach. fix(core): retry mat view refresh on transient errors instead of invalidating questdb#7275 reclassified a refresh OOM as a transient, retried failure: the refresh is deferred by cairo.mat.view.refresh.busy.retry.timeout and the view is invalidated only after cairo.mat.view.refresh.busy.retry.limit consecutive failures (MatViewRefreshJob.isRetriableRefreshError, pinned by WorkloadMemoryTrackerTest). A live view refresh breach, by contrast, invalidates immediately (LiveViewRefreshJob). The breach paragraph is now a per-workload list.
  • ALTER USER ... SET MEMORY LIMIT on an external (SSO/OIDC) user is rejected (AccessListModel.setMemoryLimit: Cannot set memory limit for external user). The docs previously only said such users inherit from groups; they now say the direct form is refused, on both rbac.md and alter-user.md.
  • Coverage note trimmed. The earlier "coverage is best-effort" paragraph, with its list of tracked allocation sites, is replaced by the one fact an operator needs: memory-mapped memory, such as the column files a query reads, does not count toward a limit.
  • WAL apply limit rarely fires. WAL apply runs only simple UPDATEs, metadata ALTERs, and commits, so the entry now says the limit mainly isolates WAL apply from the query budget.
  • Upgrade note. The memory_limit column is added by an automatic migration (SysMig6) on the first primary boot or on promote; an ACL statement refused with a message naming the memory_limit column migration means the node is not yet migrated. The window normally closes once WAL apply catches up, so retry first; a persistent refusal calls for a restart, which re-runs the migration.
  • query_activity's two memory columns are null for nested registrations, which share the outer query's budget.

Confirmed unchanged and still accurate: the SET MEMORY LIMIT grammar and permission name, override-not-min precedence and 0/UNLIMITED semantics (EntMemoryTrackerProvider.effectiveLimit), the memory_limit column on all five SHOW results including the filtered forms and the enabled/external_alias/grant_option neighbours (AbstractShowEntitiesCursorFactory, AbstractShowLinkedEntitiesCursorFactory, ShowAclTest), the COPY ... TO and non-WAL UPDATE coverage, the breach message format (Unsafe.checkPerQueryAllocLimit), reloadability of every limit (DynamicPropServerConfiguration), and the built-in admin being unlimitable.

Dependencies

Documents:

Web-console syntax highlighting for the new clause: questdb/sql-parser#27

Verification

yarn build, which this repo configures to throw on broken links, broken markdown links, and broken anchors.

Review follow-up

A level-2 /review-pr pass (six agents, findings verified against OSS fef82615 and Enterprise main) found no wrong technical claims. Its major findings are fixed in 4be3ffa: stale SHOW SERVICE ACCOUNTS tables on the grant and revoke assume-service-account pages, the OUT OF MEMORY error tag literal and the wal_tables() errorTag/errorMessage columns, the queryId mapping for materialized view refreshes, CREATE GROUP ... WITH EXTERNAL ALIAS, the two migration-window refusal messages, the RBAC page description, and the query_activity column order with the missing is_wal column.

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

🚀 Build success!

Latest successful preview: https://preview-462--questdb-documentation.netlify.app/docs/

Commit SHA: 339231a

📦 Build generates a preview & updates the link on each commit.

puzpuzpuz and others added 17 commits June 5, 2026 16:39
- show.md: SHOW SERVICE ACCOUNTS <user|group> returns name, grant_option and
  SHOW GROUPS <user> returns name, external_alias; only the bare listings carry
  the new memory_limit column
- meta.md: query_activity example projects memory_used/memory_limit
- rbac.md: alphabetize ALTER entries in the SQL commands reference
- alter-user.md: add the --- separator before Syntax for parity with siblings
- capacity-planning.md: link the RAM section to the memory limits docs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror questdb/questdb-enterprise@fb4e050: the effective per-principal
memory limit resolves by strict precedence (user's own limit -> group
limit -> global workload limit) rather than the most-restrictive value.
A more specific limit, when set, fully overrides the broader one and
binds even when larger, so a per-entity override can raise a principal's
ceiling above the workload limit, not only lower it. Service accounts
never inherit group limits.

Updated rbac.md, alter-user/group/service-account.md, show.md, meta.md,
and cairo-engine.md accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mory limit

Reviewer note on questdb-enterprise#1039: the memory-limits section named
only mat-view refresh and WAL apply as running under internal (non-principal)
contexts, but UPDATE applied through the WAL runs under the allow-all root
context too (OperationExecutor.getRootContext, reports limit 0), so a large
UPDATE is not capped by a SET MEMORY LIMIT override. Name UPDATE explicitly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the gaps

Three corrections, each checked against the Enterprise implementation rather
than inferred:

- `UPDATE` was described as never picking up a per-principal limit. That is
  false for a non-WAL table: `UpdateOperation.apply()` passes the caller's own
  SqlExecutionContext to `UpdateOperatorImpl.executeUpdate`, which calls
  `queryRegistry.register(...)`, which acquires a QUERY tracker from that
  context's security context. So a non-WAL UPDATE IS capped, and an operator
  sizing one from the old text would meet an unexpected OOM abort. Only the WAL
  path -- the default table type -- runs under the WAL apply job's tracker.
  Replaced with a section that splits what a per-principal limit covers from
  what it does not.

- `SHOW GROUPS <user>` and `SHOW SERVICE ACCOUNTS <user | group>` were shown
  without a `memory_limit` column, and the prose said so explicitly. They carry
  it: AbstractShowLinkedEntitiesCursorFactory adds it to both GROUP_METADATA and
  METADATA, pinned by ShowAclTest. Since these are exactly the results that
  break a positional client, the omission hid half the breaking change.

- The example tables disagreed with each other on the same entity's limit; made
  them consistent.

Documents four things the page did not mention at all:

- `COPY ... TO` exports are capped by the issuing principal's limit, so a limit
  must be sized for exports and not only for interactive queries.
- `SET MEMORY LIMIT` is self-escalating. It takes no entity name, so its holder
  can set any principal's limit including its own, and a per-entity limit
  overrides the workload limit rather than tightening it -- a non-admin holding
  it can raise its own ceiling above `cairo.query.memory.limit.bytes`. Plus the
  `GRANT ALL` expansion, which means an upgrade does not confer it.
- The breaking change: five results gain a column on upgrade, and positional
  clients must be updated.
- `sys.acl_entities` stores the value, readable only by the built-in admin.

Also notes that `memory_limit` means the effective limit in `SHOW USERS` but the
entity's own limit in `SHOW GROUPS` / `SHOW SERVICE ACCOUNTS`, and that an
external user's inherited limit refreshes at next login rather than live.

Verified with `yarn build`, which is configured to throw on broken links and
anchors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rise PRs

Both source PRs merged after the last revision of these docs, and OSS master
moved on afterwards. Corrections after re-checking every claim against
questdb-enterprise main and questdb master:

- There are four workload limits, not three: live views added
  cairo.live.view.refresh.memory.limit.bytes. Point at the live-views config
  page, which already documents it, and name live view refresh wherever the
  workloads are listed, including the RBAC "not covered by a principal
  limit" list.
- A mat view refresh breach is now a transient, retried failure that
  invalidates the view only after the busy-retry budget is spent
  (questdb/questdb#7275); a live view refresh breach invalidates
  immediately. Replace the one-line breach sentence with a per-workload list.
- ALTER USER ... SET MEMORY LIMIT on an external (SSO/OIDC) user is
  rejected; say so on rbac.md and alter-user.md.
- Coverage: add covered-column decode buffers, name the vectorized GROUP BY
  hash table gap, and note that the WAL apply limit rarely fires in practice.
- Add the upgrade note on the automatic acl_entities migration and the
  refusal an operator sees on a not-yet-migrated node.
- query_activity memory columns are null for nested registrations.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XECBHc6Z36vw4i4KpduBzH
… note

Drop the "coverage is best-effort" paragraph and its list of tracked
allocation sites. The one thing an operator needs to know is that
memory-mapped memory, such as the column files a query reads, does not count
toward a limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XECBHc6Z36vw4i4KpduBzH
The memory-limits section describes a refresh breach as retried after
cairo.mat.view.refresh.busy.retry.timeout and invalidated only after
cairo.mat.view.refresh.busy.retry.limit consecutive failures, but neither key
was documented. Add both to the materialized-views configuration page, note
the deprecated no-op cairo.mat.view.refresh.oom.retry.timeout, and link the
breach description to the new entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XECBHc6Z36vw4i4KpduBzH
Correct three statements against the merged code: a WAL apply breach backs
off and retries under memory-pressure control before the table is suspended,
the WAL apply limit covers only the SQL run inside a batch and not data
commits, and query_activity's memory columns are null for SQL run under a
background workload's tracker rather than for subqueries.

Document ram.usage.limit.bytes and ram.usage.limit.percent, list the four
workload tokens of the breach message and what queryId means per workload,
state when a changed per-principal limit reaches open sessions, that the
statement runs on the primary only, and restore the upgrade migration note.
Say that SET MEMORY LIMIT UNLIMITED clears the principal's own limit and the
group or workload limit then applies. Show external_alias as empty rather
than null, update the stale SHOW result tables on create-group, add-user,
grant-assume-service-account, and cancel-query, refresh the SHOW description
bullets and grammar, give the ALTER pages specific descriptions, remove
em-dashes, and add the changelog entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZbDYwQEcex9qR1HPtcBqi
Keep the full limit-resolution rule on the RBAC page and trim the ALTER
pages, query_activity, and SHOW to page-specific facts plus a link. State
once per page that the K, M, and G suffixes multiply by 1024. Use john
throughout and rename the colliding service account example to client_app.
Deep-link the cairo.query.memory.limit.bytes key to its own anchor. Link the
refresh vocabulary to the refresh strategies section and add the memory
breach to both concept pages' invalidation lists. Document
cairo.mat.view.max.refresh.retries, which bounds the in-call retries. Link
the OIDC guide to the CREATE GROUP and ALTER GROUP pages. Note that COPY TO
exports do not appear in query_activity and that SHOW USER does not carry
the memory_limit column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZbDYwQEcex9qR1HPtcBqi
- Add the trailing memory_limit column to the SHOW SERVICE ACCOUNTS
  tables that the breaking-change update missed on the grant and revoke
  assume-service-account pages
- Fix the WAL apply suspension error tag: wal_tables() reports
  `OUT OF MEMORY` in its errorTag column, not `OUT_OF_MEMORY`; document
  the errorTag and errorMessage columns of wal_tables()
- Name the queryId source for every workload, including the materialized
  view refresh, which was omitted
- Document CREATE GROUP ... WITH EXTERNAL ALIAS, which the new OIDC
  pointer promised but the page lacked, with the IF NOT EXISTS exclusion
- Quote the two migration-window refusal messages and say that a
  persistent refusal calls for a restart, matching the server's own text
- Mention memory limits in the RBAC page description
- List query_activity columns in their real order, add the missing
  is_wal column, and make the self-listing example row match the query

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAJqWAFJXYVbvvUXqDZCYh
Every other row in the section table is a short category label, so
naming one feature in the Cairo engine row broke the pattern. Memory
limits stay discoverable through the page description, the capacity
planning pointer, and the changelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAJqWAFJXYVbvvUXqDZCYh
- rbac: a user can read its own effective cap from query_activity; describe
  the two migration windows and the statements each refuses; name the breach
  error a capped query fails with
- cairo-engine: ram.usage.limit.* counts tracked native allocations, not RSS,
  and a global breach lands on whichever workload allocates last
- meta: wal_tables is the only source of errorTag/errorMessage; add both and
  memoryPressure to the example output
- monitoring-alerting: link the memory limits from suspended tables, invalid
  materialized views, and memory pressure

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ7EBdj733Bc1VJ2eU9bTp
Correct the wal_tables() column name (bufferedTxnSize, not writerLagTxnCount)
and the memoryPressure value of the OOM-suspended example row, document when an
external user's inherited group limit takes effect, and add the previously
undocumented cairo.write.back.off.timeout.on.mem.pressure key.

Clarify the process-wide limit naming and its cgroup-aware percentage, give a
concrete breach message with field semantics and the decimal COPY id, list the
statements the query limit covers, state that a limit is per query rather than
per principal, and add sizing and recovery guidance. Fix the per-query wording
on ALTER USER and ALTER SERVICE ACCOUNT, the ALTER GROUP fallback sentence and
intro, the ingest example name, the SHOW permission requirements, and several
descriptions. Add inbound links from COPY, UPDATE, WAL, OIDC, RESUME WAL,
migrate-to-enterprise and Ignition pages, and complete the changelog entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqg12uAgpQjZhtpDuKctTq
- cairo-engine: the query limit's statement list read as exhaustive but
  omitted COPY ... TO and other registered statements; make it "every
  statement that appears in query_activity, including ...". State that a
  breach leaves the client connection open. Add a signpost to the section
  from the page intro, and a navigation line on the configuration
  overview page.
- show.md / meta.md: the memory_limit column has a different null
  contract in SHOW USERS (excludes the workload limit) and query_activity
  (includes it); cross-reference each from the other. Add the positional
  breaking-change note the changelog already pointed at.
- rbac: give the upgrade material its own anchored "Upgrading"
  subsection with a short breaking-change warning; link it from the
  changelog and show.md. Drop "new" from the SHOW SERVICE ACCOUNTS
  groupName filter, which has existed since 2023.
- Recovery steps on the materialized view concept, live view concept,
  and monitoring pages now say to raise the limit before the full
  refresh, which runs under the same limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161gm7KnBBfUrQy7o16Bt5r
- cairo-engine: the query limit coverage sentence contradicted the COPY and
  RBAC pages, which say exports do not appear in query_activity, and listed
  CREATE MATERIALIZED VIEW although its initial population runs as a refresh
  under cairo.mat.view.refresh.memory.limit.bytes
- alter-mat-view-resume-wal: replace the stale four-column wal_tables()
  table with the projected form used on the table RESUME WAL page
- meta: approx_percentile takes a fraction, so the query_activity example
  row reads 0.5 rather than 50
- rbac: the SHOW results carry memory_limit from the first start of the
  upgraded binary; only the sys.acl_entities column waits for the migration

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KVKCahCEYveYB9JasZCss1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant