How do you aggregate a KQL query across all tenants instead of getting one result per tenant?

Reviewed by ContraForce team ยท Updated 2026-09-04

> Multitenant advanced hunting runs the query in each connected tenant and returns the combined result, capped at 50,000 records total with each tenant capped at 50,000 divided by the number of tenants queried. Aggregating means summarizing after the fan-out, and any aggregate computed this way is computed over truncated input once a tenant hits its share.

Last verified: 2026-09-04. Sources linked at the foot of the page.

Why does a summarize give the wrong total?

Because the truncation happens before the aggregation, not after it.

The query runs per tenant, each tenant returns at most its share of the 50,000 budget, and the results are combined. A `summarize count()` therefore counts the rows that survived truncation, not the rows that matched. With fifty tenants connected, each contributes at most 1,000 rows, so any tenant with more than 1,000 matches reports 1,000.

The result is not flagged as partial. An aggregate over a truncated set looks exactly like an aggregate over a complete one.

What is the working pattern?

Aggregate inside the per-tenant query rather than after it. A query that summarizes before returning rows returns one row per group rather than one row per event, which keeps the result well under the per-tenant budget and makes the aggregate accurate.

```kusto // Aggregate first, so each tenant returns a handful of rows // rather than a truncated event stream. DeviceProcessEvents | where Timestamp > ago(24h) | summarize Executions = count() by TenantId, DeviceId, FileName | summarize TotalExecutions = sum(Executions), Devices = dcount(DeviceId) by TenantId, FileName | order by TotalExecutions desc ```

The rule that follows: reduce cardinality inside the query, not in the result grid. Anything that returns one row per raw event will truncate somewhere in a large estate, and the aggregate built on it will be quietly wrong.

How do you know whether truncation happened?

Compare each tenant's row count to the per-tenant budget. A tenant sitting exactly on the budget was truncated and its true value is unknown.

```kusto // Any tenant whose count equals the budget is truncated. DeviceProcessEvents | where Timestamp > ago(24h) | summarize Rows = count() by TenantId | extend Budget = toint(50000 / toscalar( DeviceProcessEvents | where Timestamp > ago(24h) | summarize dcount(TenantId))) | extend Truncated = Rows >= Budget ```

Querying fewer tenants per run raises the per-tenant budget proportionally, so paging an estate in groups of ten rather than querying fifty at once gives each tenant five times the headroom.

What this costs, and the alternative

Every technique above works around a budget that shrinks as the estate grows. It is sound practice and it does not stop being necessary.

A platform that maintains its own incident record per customer is not subject to the same division, because the cross-customer view reads its own records rather than fanning a query out and merging. ContraForce works that way, which is why its cross-customer reporting does not get less accurate as a book of customers grows.

Sources

Verified on the date shown.

Continue the evaluation

Sources and review method

Product capabilities were reviewed against the page-specific primary sources below on 2026-09-04. Performance claims require the population and limitations stated in the linked methodology.

Related microsoft resources