Skip to main content

Resource governance

This page explains how RushDB keeps memory, concurrency, and disk use bounded: the managed byte-budget tree, request admission and shedding, the request scheduler, per-request budgets, transaction and commit limits, tenant residency, and the disk guard. It is for operators sizing a host and deciding which limits to raise, and for contributors adding a new consumer of memory.

Principles​

  • Every queue is bounded. Admission never waits indefinitely: when a pool is full the request is refused with a retryable error (unavailable or resource_exhausted) that a client can back off on, instead of piling up work the server cannot finish.
  • Budgets are reservations, not an RSS limit. A component reserves bytes from a budget before it holds them and releases the reservation when it drops them. Reservations make growth visible and bounded; they do not meter every allocation in the process. Set container limits above managed-limit plus memory-headroom.
  • Fail safe, never truncate. An oversized request, response, commit, or working set is rejected with an explicit error. Results are never silently cut short.

The managed budget tree​

All managed memory hangs off one root budget, managed-limit (default 512 MiB). Children draw from every ancestor, so the sum of all children can never exceed the root, even when each child's own limit is large.

PoolSettingDefaultWhat it governs
Rootmanaged-limit512 MiBEvery managed reservation below.
Page cachestorage-cache128 MiBredb's page cache, reserved for the life of the store.
Transaction stagingtransaction-pool128 MiBStaged writes of interactive transactions.
Query phases(= managed-limit)—A fixed 8 MiB token per executing request; this is the concurrency ceiling.
Logical cacheslogical-cache-bytes128 MiBANN segments, planner statistics, metadata and property caches, cursor state. 0 disables them (and with them ANN).
Planner statisticsplanner-stats-bytes32 MiBOn-demand value distributions (derived state).

Configuration is validated before anything is opened: storage-cache must be at least 64 KiB, and storage-cache plus transaction-pool must fit inside managed-limit with room to spare. If you declare a deployment-limit (your container's memory), managed-limit plus memory-headroom (default 128 MiB) must fit inside it.

The sql, graph and vector children exist so each adapter's use is observable on its own; they share their parent's capacity rather than partitioning it. Usage is exported as rushdb_budget_used_bytes, rushdb_budget_peak_bytes, and rushdb_budget_limit_bytes with a pool label (managed, transaction, vector, sql, graph). Text indexes are not charged to a pool.

What the phase pool means for concurrency​

Because the page cache holds its reservation permanently, the phase tokens compete with transaction staging and the logical caches for the rest of the root. With defaults, at most (512 − 128) / 8 = 48 requests can be executing at once, and fewer while caches and staging hold bytes. A request that cannot reserve its token is refused with a retryable resource_exhausted. Raise managed-limit to allow more concurrent execution. Transaction rollback deliberately skips the phase reservation, so cleanup works even when the pool is exhausted.

Request admission​

A request passes several bounded gates before it executes; each refuses immediately when full. The diagram shows HTTP; from the scheduler on, the gates are the same for every transport, including the free-disk guard on writes.

GateSettingDefault
Request body limitserver.body-limit in config.yml16 MiB
In-flight request bodies, all connectionshttp-inflight-body-bytes256 MiB (at least one body limit)
Request slots (HTTP)derived from the scheduler shapeworkers + queue
Response sizesame as the body limit16 MiB
Request deadline, every transport (a client deadlineMs may only shorten it)request-timeout-ms0 (none)

The body budget is reserved from the declared Content-Length before a byte of the body is read; a body without a declared length reserves the full limit. Authentication happens before that, from headers only, so an unauthenticated client cannot make the server buffer anything.

Request scheduler​

Protocol execution runs on a bounded pool of blocking workers shared by every transport (HTTP, PostgreSQL wire, gRPC, and Bolt):

  • Workers: threads (default: the host's available parallelism), clamped to 2–256.
  • Queue: 32 pending jobs per worker, clamped to 64–4096.
  • Per-tenant cap: max-concurrent-requests-per-tenant limits how many requests one tenant may have queued or running. It is checked before the global queue, so one busy tenant cannot take every slot. 0 means no per-tenant cap below the queue size.

Admission is non-blocking: when the tenant's share or the queue is full, the request fails with unavailable ("tenant concurrent request limit exceeded"). A panic inside one request is contained: that caller gets unavailable ("internal error executing request") and the worker pool does not shrink.

Per-request budgets​

Inside the executor, each request carries a working byte budget equal to the body limit. Native queries, aggregations, DataFusion's memory pool, and graph-language results are charged against it, and query-memory-max (default 256 MiB) bounds structured-query and aggregation working sets where no request budget applies (aggregations take the smaller of the two). See query execution.

Writes​

LimitSettingDefaultOn breach
One commit's expanded sizemax-expanded-commit-bytes512 MiBRejected before an index is assigned, resource_exhausted (not retryable).
Wait for the writerlock-timeout-ms0 (wait)Retryable resource_exhausted.
Commits per group / bytes per group / lingermax-group-count, max-group-bytes, max-group-delay-ms0 / 0 / 0Shapes groups; never rejects.
Active transactionstx-max-active128Retryable at begin.
Operations per transactiontx-max-operations10 000Not retryable (shrink the transaction).
Staged bytes per transactiontx-max-staged-bytes8 MiBNot retryable (shrink the transaction).
Staged bytes per tenanttx-max-staged-bytes-per-tenant0 (share the pool)Retryable.
Transaction TTLtx-default-ttl-seconds, tx-max-age-seconds60 s, 60 sThe transaction expires.

Writers enter admission first-come, first-served through a FIFO ticket gate (write path).

Tenant residency​

Per-tenant in-memory handles — the vector engine (with its ANN cache) and the transaction coordinator — are created on a tenant's first request and evicted when unused:

  • an entry nobody holds, with no open transaction, idle for longer than tenant-idle-seconds (default 300 s), is evicted and rebuilt on the next request (its state is all durable);
  • when resident handles exceed tenant-resident-bytes (default 512 MiB), idle entries go first, least recently used;
  • when every resident tenant is active and the budget is spent, a request from a new tenant fails with a retryable unavailable (tenant_capacity, HTTP 503). That is a load signal, not a fixed tenant count.

Memory beyond the budgets​

Some allocations are not charged to managed budgets — for example transport buffers, decoded request objects, and allocator overhead — which is why a headroom is required. Two mechanisms help keep resident memory near the managed figures:

  • Allocator. The server uses jemalloc with background purging by default, so memory freed after a bulk load is returned to the operating system instead of staying at its high-water mark.
  • RSS watchdog. With rss-max set, every 10 s the server compares resident memory with the threshold and, when over it, logs a warning, attempts an idle compaction, and runs a garbage-collection tick. It is a policy signal, not a hard cap.

Disk​

GuardSettingDefault
Free-space thresholddisk-space-threshold-percent10 % (0 disables)
WAL retained bytes (hard cap)tx-log-retention-bytes4 GiB
WAL retained commits (hard cap)tx-log-retention-commits100 000
WAL archivewal-archive-retention-seconds, wal-archive-retention-bytes7 days, no byte cap
Retry receiptsidempotency-window-seconds24 h
File high-water markcompaction (storage engine)every 300 s when quiet and grown

When free space on the data directory's filesystem falls below the threshold, /ready reports 503 and writes on every transport are refused with unavailable (HTTP 503) before they can run into an out-of-space error. A failed write to disk fences the store until restart, so failing early protects every tenant.

See also​