TL;DR. Power BI usage metrics cannot tell you which reports and seats went unused for 90 days: the usage metrics report keeps 30 days, the admin activity API accepts only the last 28, and the admin unused-items API looks back 30. The Purview audit log keeps Power BI events for 180 days, so a 90-day audit can run today instead of after 90 days of daily exports. This runbook pulls that log, finds reports nobody opened and Pro seats nobody used, and prices both: $14 a month per seat at list price, plus the warehouse queries an orphaned import model keeps running for up to two months.

Power BI usage metrics show who opened a report in the last 30 days, which is too short to call a seat or a report unused. For prices and plans, Power BI pricing covers Pro, PPU and Fabric. This post is the audit behind it: which source holds 90 days of history, the joins that turn events into a list of seats and reports, and what each item on that list costs.

Everything below comes from Microsoft's documentation, read November 10, 2026. The scripts follow the documented cmdlets and APIs but were not run against a live tenant, and the tenant numbers are a worked example, not a measurement.

What Power BI usage metrics show, and the 30-day limit

The usage metrics report answers one question per report: who opened it, how often, from where. Microsoft states the window plainly: "The Usage Metrics Report semantic model contains usage data for the last 30 days only. Data older than 30 days is automatically removed."

The report also hides the thing you are looking for. Microsoft's FAQ on the same page says the improved report "only includes reports that have been opened in the past 30 days." A report nobody opened does not appear as a zero; it is missing. You also need admin, member or contributor rights on each workspace to open it, and a Pro or PPU license.

The tenant-wide options have the same horizon:

SourceHistoryScope
Usage metrics report30 daysone workspace at a time
Get Unused Artifacts as Admin (preview)30 days, fixedone workspace per call
Feature usage and adoption report (preview)30 daystenant
Get Activity Events admin APIlast 28 days, one UTC day per calltenant
Purview unified audit log, Audit (Standard)180 daystenant, all Microsoft 365 services

Microsoft notes that the feature usage report "remains in preview, won't become GA, and won't be expanded with additional data."

Horizontal bar chart of how many days of Power BI usage history each Microsoft source keeps. The usage metrics report, the unused artifacts API and the feature usage and adoption report keep 30 days, the activity events API 28 days. Only the Purview audit log, at 180 days, crosses the 60-day refresh pause line and the 90-day audit window.

Pull 180 days of Power BI activity from the Purview audit log

The usual advice is to start a daily export of the activity log and wait 90 days. That advice predates a change in Purview. Microsoft's audit overview says: "Audit (Standard) logs generated on or after October 17, 2023, follow the new default retention of 180 days." Power BI events land there as the PowerBIAudit record type (Management Activity API schema).

Microsoft's own Power BI auditing guide is out of date on this point. Its tenant-level auditing guidance still says "The most recent 90 days of history are available with Audit (Standard)." The Purview pages are the authority on Purview retention, and they say 180. If your tenant's audit search returns less, trust the search result over either page.

One more conflict to check: the same guide says Search-UnifiedAuditLog retrieves "a maximum of 90 days of history." Run the search for a single day 170 days ago first. If it returns records, you have the full 180 days; if not, you still have 90, which is enough for the audit below, just without the quarterly hold check.

There are two costs to this route. Searching the log needs the Audit Logs role in Exchange Online (Track user activities in Fabric), and the auditing guide warns that "By default, Power BI administrators don't have permission to access the unified audit log search." The Search-UnifiedAuditLog cmdlet also pages: ResultSize stops at 5,000 per call, and one session with ReturnLargeSet reaches "a maximum of 50,000 results." Searching one day per session usually stays under that cap; if a day returns 50,000 records, split it into shorter windows.

  1. Export 180 days, one UTC day per session, from Exchange Online PowerShell:
Connect-ExchangeOnline
$today = (Get-Date).ToUniversalTime().Date
$events = foreach ($d in 1..180) {
  $start = $today.AddDays(-$d)
  do {
    $page = Search-UnifiedAuditLog -StartDate $start -EndDate $start.AddDays(1) `
      -RecordType PowerBIAudit -SessionId "pbi-$d" `
      -SessionCommand ReturnLargeSet -ResultSize 5000
    $page | ForEach-Object { $_.AuditData | ConvertFrom-Json }
  } while ($page.Count -eq 5000)
}
$events | Select-Object CreationTime, UserId, Operation, WorkSpaceName, ReportName, ReportId, DatasetName, DatasetId |
  Export-Csv pbi-audit-180d.csv -NoTypeInformation
  1. Open one record and check which fields it carries. The Power BI schema for audit records documents ReportName, DatasetName and WorkSpaceName; the activity events API sample also shows ReportId and DatasetId. Join on the IDs when they are present, and on workspace plus name when they are not.
  2. Keep the file. Rerun it monthly and append, and the next audit can look back further than 180 days.

Find reports nobody opened in 90 days

The audit log only lists events, so a report nobody opened leaves no trace in it. The inventory comes from the admin APIs: Get Reports As Admin returns every report in the organization with its datasetId, and Get Datasets As Admin returns each model's isRefreshable flag. Both allow "Maximum 50 requests per hour or 5 requests per minute, per tenant," so page with $top and $skip rather than calling per workspace.

  1. Pull both lists once and save them.
  2. From the audit file, take the latest ViewReport event per report.
  3. Mark each report: opened in the last 90 days, opened between day 91 and day 180 (hold), or not opened in 180 days (candidate).
  4. Group the candidates by datasetId. A model whose every report is a candidate and whose isRefreshable is true is an import model nobody reads. Microsoft defines the flag as true when the model "is either recently refreshed or is configured for automatic refresh," in Import mode only.
  5. For those models, read the schedule with Get Refresh Schedule In Group. If enabled is true, the model is still refreshing, and the days and times arrays give you refreshes per week: this is the cost to stop. If it is false, nothing is refreshing now. Archive the model anyway, because the next visit restarts the schedule for another two months.

Do not open a candidate report to check it. Power BI pauses the schedule of a model nobody visits, and the data refresh docs say that "To resume scheduled refresh, visit a report or dashboard built using this semantic model." One look from the person running the audit restarts the refresh. Read the metadata instead, or ask the owner.

Find Pro seats nobody uses: count any activity, not only views

The license list comes from Microsoft Graph. The Power BI Pro SKU is POWER_BI_PRO, ID f8a1db68-be16-40ed-86d5-cb42ce701560, and Premium Per User is PBI_PREMIUM_PER_USER, ID c1d032e0-5619-4761-9b5c-75b6831e1711 (license service plan reference). Graph's list users call filters on assignedLicenses/any(u:u/skuId eq ...).

Connect-MgGraph -Scopes User.Read.All
$pro = Get-MgUser -All -Property UserPrincipalName `
  -Filter "assignedLicenses/any(x:x/skuId eq f8a1db68-be16-40ed-86d5-cb42ce701560)"
$audit = Import-Csv pbi-audit-180d.csv
$last = @{}
foreach ($e in $audit) {
  $t = [datetime]$e.CreationTime
  if (-not $last[$e.UserId] -or $t -gt $last[$e.UserId]) { $last[$e.UserId] = $t }
}
$cut90 = (Get-Date).AddDays(-90)
$pro | ForEach-Object {
  $seen = $last[$_.UserPrincipalName]
  [pscustomobject]@{ User = $_.UserPrincipalName; LastActivity = $seen
    Status = if (-not $seen) { 'reclaim' } elseif ($seen -lt $cut90) { 'hold' } else { 'active' } }
} | Export-Csv pro-seats.csv -NoTypeInformation

Last activity of any kind, not last view, decides a seat. The Fabric operation list logs AnalyzeInExcel when someone builds an Excel workbook on a model, ExportReport for exports, and CreateEmailSubscription for subscriptions. An analyst who works mostly through Excel may log few or no ViewReport events, and a views-only filter would flag them.

Two more checks before the list is final:

  • E5 holders. Microsoft's licensing guide says "Power BI Pro is included in Microsoft 365 E5." A standalone Pro on an E5 user is a duplicate, whatever the activity.
  • Readers on F64 or larger. Microsoft's Fabric licensing page says: "On F64 or larger, users with only a Free license and a viewer role can view Power BI content." A Pro holder who only views content there has a seat that is used but not needed. Power BI pricing works out when that capacity pays. The CapacityId field shows where a view happened, but the operation list warns it "will return null if the report or dashboard is viewed from a Power BI app," so app views need a workspace lookup.

What an unused report still costs: $14 a seat and a refresh that runs for two months

Seats are the easy part to price. A Pro seat lists at "$14.00 user/month, paid yearly" on Microsoft's pricing page, and a Premium Per User seat at "$24.00 user/month, paid yearly".

Consider a hypothetical tenant with 400 Pro seats, no E5, and content on shared capacity. Assume the export splits it like this:

StepUsersMonthly list cost
Pro licenses in Graph400400 x $14 = $5,600
Any Power BI activity in the last 90 days352kept
No activity in 90 days48
...but activity between day 91 and day 18010held, likely quarterly
No activity in 180 days3838 x $14 = $532

In this example, reclaiming the 38 seats saves $532 a month, $6,384 a year, 9.5% of the Pro bill. The split is an assumption; the arithmetic is the method.

The report side costs less per item but runs without anyone noticing. Consider an orphaned import model whose refresh query scans 150 GiB in BigQuery, scheduled eight times a day, the maximum on shared capacity. That is 1,200 GiB a day, or 1.17 TiB. At BigQuery's on-demand rate in US regions, "$6.25 / 1 tebibyte" (BigQuery pricing), it costs $7.32 a day.

Power BI caps that tail, but late. The data refresh docs say: "After two months, when no user has visited any dashboard or report (including via an app or embedded scenario) built on the semantic model, Power BI considers the semantic model inactive," and the service pauses the schedule. At $7.32 a day, 60 days of refreshes (about two months) cost $439.45 before the pause. On a Premium, PPU or Fabric capacity the schedule allows up to 48 refreshes a day, six times as many, and the same model would cost $43.95 a day on the warehouse, $2,636.72 over 60 days. Every stray visit restarts the clock.

These figures ignore BigQuery's free first TiB per month and assume every refresh scans the full 150 GiB. A model with one live report on it never pauses and is not waste; the audit only targets models whose reports are all unread. For how BI refreshes show up on the warehouse bill elsewhere, see cloud waste in data infrastructure and BigQuery on-demand vs editions.

Keep quarterly readers, Excel users and embedded reports

The strongest objection to this audit is that it costs more than it saves. The Audit Logs role is granted in Exchange Online, outside the Power BI admin portal, and the 38 seats in the example save $532 a month. For a 40-seat tenant the honest answer is often to skip it and review seats at renewal. The case for running it is that the script runs again every quarter at no extra cost, and the refresh tail does not show up on the Power BI bill at all; it shows up on the warehouse bill, where nobody connects it to a report.

What to hold back from any cut list:

  • Quarterly and year-end readers. A 90-day window misses a reader who opens the Q4 pack once a year. The 180-day file catches quarterly use; yearly use needs your own archive.
  • Excel and export users. Anyone with AnalyzeInExcel or ExportReport events is a user, whatever the view count says.
  • Embedded reports. Visits through an app or an embedded scenario count as visits and keep the model's refresh running, and the usage metrics report splits views by platform (PowerBI.com, Mobile, Embedded). Check the platform before calling a report dead.
  • Owners' notice. Send the list to report owners and managers before removing a license or a report.

Tableau pricing runs the same seat audit for Tableau, where Tableau Cloud suspends full extract refreshes that run more often than weekly once a workbook has been inactive for 32 days, and Looker pricing does it for Looker's unused content.

Datapace is building one map of your data estate with usage and cost attached, and agents that propose changes as pull requests your team reviews and merges. Each report, its semantic model, its refresh schedule and the warehouse queries behind it belong on that map, priced together. To price the seats first, start with Power BI pricing.

Sources

  1. Microsoft Learn, Monitor usage metrics in Power BI workspaces, read November 10, 2026.
  2. Microsoft Learn, Admin - Get Activity Events, Admin - Groups GetUnusedArtifactsAsAdmin, Get Reports As Admin, Get Datasets As Admin and Get Refresh Schedule In Group.
  3. Microsoft Learn, Feature usage and adoption report, Track user activities in Microsoft Fabric and Operation list.
  4. Microsoft Learn, Learn about auditing solutions in Microsoft Purview, Search-UnifiedAuditLog and Office 365 Management Activity API schema.
  5. Microsoft Learn, Power BI implementation planning: Tenant-level auditing.
  6. Microsoft Learn, Product names and service plan identifiers for licensing, List users (Microsoft Graph) and Power BI licensing guide for organizations.
  7. Microsoft Learn, Data refresh in Power BI.
  8. Microsoft, Power BI pricing, read November 10, 2026.
  9. Google Cloud, BigQuery pricing, read November 10, 2026.