6.5 Accounting and Limits
6.5.1 Recording what was asked #
Every event query is recorded by the adaptive indexing system (§3.4).
For each WHERE predicate:
- a header field reference increments the frequency counter of that
field's column, kept under the field's path (
event.type,emitter.process.guid);event.time, whose column is always indexed, is not counted - a payload field reference increments that path's counter
Cross-type WHERE predicates are counted like any other, since they
narrow the same data by the same fields.
This applies to event queries only. The log and metric stores have fixed write-time indexes and no candidate space for a policy to explore (§4.2, §5.2), so their queries increment nothing.
Counters are in-memory and are flushed to the metadata database at each policy interval (§3.5). Query handlers never write to that database directly, which is what keeps the once-per-query update off any lock a writer thread contends for.
6.5.2 Concurrency #
eventd bounds concurrent queries — streaming and non-streaming together
— at MaxConcurrentQueries (§A).
Beyond it a query is rejected with an error rather than queued.
The per-query cost that limit is protecting is real: read-only SQLite connections, one per shard for an event query (§6.4), memory for the merge, and CPU for execution and payload decoding.
MaxStreamingQueries is enforced separately and is lower.
A streaming
query holds its resources for as long as its client stays connected,
where an ordinary one holds them for at most a timeout, so the two
populations need different bounds.
Both are global. Every authenticated caller may connect (§7.1), so
eventd also bounds the queries one caller may have running at once, at
MaxQueriesPerUser.
The caller is the user SID of the token read at connect.
The slot is taken as soon as the token is read, before the request, so
connections held open without a query count too.
SYSTEM's queries are not counted: SYSTEM is the machine, not one caller
among others. Over
the limit, the query is rejected with an error, as over the global ones.
The per-user limit counts users, not processes or sessions: two windows of the same person share one budget, and two people never share one.
Queries and ingestion are separate channels, so exhausting the query side cannot exhaust ingestion (PSPU §3.3).
6.5.3 Memory #
eventd may not be killed: it runs with oom_score_adj −1000, so if its
memory alone fills the machine, the kernel has nothing left to reclaim
and panics. What bounds a query's memory therefore has to hold however
many queries run at once.
A query in the default order holds one row per shard and nothing more (§6.4), and is never refused for the size of its result (PSPU §3.16).
Everything else a query must hold — a sorted query's rows, an
aggregation's groups, a watch batch — counts against
MaxQueryHeldBytes (§A), one budget for every running query
together.
A query takes from it in 64 KiB granules as it grows, gives back what
it no longer needs when a sorted query trims, and gives back all of it
when it ends.
A query that would take more than is left fails with an error, rather
than being truncated or answered from part of the data.
What a row or group costs is estimated from its fields and values, not measured from the allocator, so the budget is approximate. It is still a bound: with the default of 256 MiB, no number of concurrent queries holds much more than that.
A metric query reads each series' samples in order and holds none of them: a transform keeps only the sample before, and an aggregation folds each sample into its window, or its series, as it is read. What it holds is what its result needs, and that counts against the same budget: the matched series, one fold per window or per series, the latest point of each series, and the result rows. A query whose result is the samples themselves — a range without an aggregation — holds them all, and is refused when they pass the budget like a sorted query. A window aggregation over the same range holds one fold per window, however many samples fall in it.
6.5.4 Request and response sizes #
eventd rejects an inbound frame above MaxQueryRequestBytes before
allocating or reading its payload.
Outbound result records are grouped at record boundaries toward
QueryResponseTargetBytes (§A).
The target is not a limit: a complete record that exceeds it is sent
alone, never split, truncated, skipped or failed merely for exceeding
the target (PSPU §3.15–§3.16).
Only the protocol's u32 frame length is a hard outbound bound.
6.5.5 Timeouts #
Every query has a maximum execution time, QueryTimeoutMs (§A).
The clock starts once the request has been decoded and the caller's token obtained, and covers everything after: parsing, planning, access checks, cross-type pre-computation, SQL execution, merging, aggregation, pagination, projection, and transmitting the initial result set.
It bounds the initial result set only. A non-streaming query sends
end before it expires; a streaming query sends watch.
Past watch
the stream is not time-limited, and what bounds it instead is
MaxStreamingQueries, MaxDistinctStreamValues and backpressure
(§6.6).
On expiry eventd cancels the query and sends an error. Any result messages already sent are discarded by the client, since no terminal message arrived (PSPU §3.16).
Cancellation has to reach two kinds of work. SQLite work is interrupted
through sqlite3_interrupt or an equivalent progress-handler check —
the same responsiveness problem as cancelling an index build (§3.4).
Non-SQL work — MessagePack flattening, the cross-shard merge — checks
the same deadline periodically, because a query can spend most of its
time in neither the database nor the kernel.
Large scans over unindexed fields are the main timeout risk, and the adaptive indexing system reduces it over time by indexing whatever keeps being filtered on — which is also why a timeout is a signal worth watching rather than merely an error to retry.