Slow Power BI reports have five possible locations, measured in order: visual rendering, the DAX query, the model shape, the source or gateway, then capacity. Guessing costs weeks. EPC Group is a Houston-based Microsoft consulting firm operating since 1997, with six Microsoft Solutions Partner designations and 216+ M&A tenant migrations covering 1.83M users. This is the triage sequence we run.
Last updated: 2026-07-31
Key facts
- Fabric smooths interactive operations over five to 64 minutes and background operations over a full 24 hours — which is why a report is slow long after the job that caused it finished.
- Throttling has four published stages, keyed to future capacity consumed: overage protection to 10 minutes, 20-second interactive delay from 10 to 60 minutes, interactive rejection from 60 minutes to 24 hours, total rejection beyond 24 hours.
- Fabric evaluates capacity in 30-second timepoints — 2,880 in 24 hours. The Capacity Metrics app uses the same period, so its numbers and platform behaviour agree.
- DirectQuery enforces a one-million-row return limit and a 225-second query timeout. Reports exceeding either fail rather than degrade.
- A single semantic model on F64 is capped at 25 GB; F128 is 50 GB, F256 is 100 GB, F512 is 200 GB.
- The gateway has a concurrency limit of 30 refreshes. Past it, the fix is another cluster member, not a bigger machine.
- Best Practice Analyzer checks 60+ rules across performance, DAX expressions, error prevention, maintenance, and formatting — and now runs in a Fabric notebook against a published model, alongside Memory Analyzer.
Quick facts
| Question | Answer |
|---|---|
| First tool to open | Power BI Desktop Performance Analyzer |
| What it splits | DAX query time vs visual display time vs "other" |
| Second tool | DAX Studio — server timings, and VertiPaq Analyzer for model size by column |
| Model audit tool | Best Practice Analyzer (Tabular Editor, or a Fabric notebook) |
| Capacity tool | Microsoft Fabric Capacity Metrics app |
| Power Query tool | Query Diagnostics, plus Diagnose Step |
| Throttle stage that delays users 20 seconds | 10 to 60 minutes of future capacity consumed |
| Throttle stage that rejects user queries | 60 minutes to 24 hours |
| Interactive smoothing window | 5 to 64 minutes |
| Background smoothing window | 24 hours |
| Can Performance Analyzer measure capacity? | No — it runs in Desktop and cannot monitor PPU activity or capacity |
The Five-Probe Latency Trace
Every slow-report engagement EPC Group runs follows the same order, and the order is the method: each probe has a stop condition, and you do not advance until it is clean. Tuning DAX on a throttled capacity is wasted work; scaling capacity to fix a bi-directional filter is wasted money.
Probe 1 — Visual. In Power BI Desktop, start Performance Analyzer, refresh visuals, and read the split for the slowest one: DAX query duration versus visual display duration. Stop condition: if display time dominates, the problem is the page, not the data. Cut the number of visuals, apply a Top N filter to tables so they stop materialising every row, move detail to drillthrough pages and page tooltips, and time every custom visual on its own. Advance only when query time dominates.
Probe 2 — Query. Take the DAX Performance Analyzer hands you and run it in DAX Studio with server timings on. Look at the storage-engine versus formula-engine split and the number of storage engine queries. Stop condition: a single expensive measure. Usual culprits: iterators that test every row, unnecessary filter-context overrides, and TopN or measure filters that force two source queries. For DirectQuery, Performance Analyzer also surfaces native SQL for SQL Server, Oracle, and Teradata.
Probe 3 — Model. Run Best Practice Analyzer and Memory Analyzer against the model. BPA carries 60+ rules across five categories; Memory Analyzer returns size by table, column, hierarchy, partition, and relationship. Stop condition: you can name the three highest-memory columns and justify each. You are hunting high-cardinality columns, wide fact tables, and calculated columns that belong upstream. Export a .vpax for a second opinion — structure and size, no data.
Probe 4 — Source and gateway. For Import, use Query Diagnostics and Diagnose Step to confirm folding. For gateway-bound sources, enable performance logging and load the diagnostics template. Stop condition: folding confirmed, spool storage not full, gateway co-located with source and tenant. Latency here is invisible in every other probe.
Probe 5 — Capacity. Open the Fabric Capacity Metrics app and read three tabs in order: Utilization (over the SKU line?), Throttling (which stage?), Overages (add, burndown, cumulative, minutes-to-burndown). Stop condition: utilisation above 100% does not by itself mean throttling — bursting is a feature. Only the Throttling tab shows whether users are being hurt.
The order matters. Probes 1–4 reduce the CU cost of the workload; probe 5 tells you whether capacity can carry what is left. Run probe 5 first and you will buy a bigger SKU to host an unoptimised model, then be back in six months.
Symptom, cause, fix
| Symptom | Root cause | Fix |
|---|---|---|
| Every visual on one page is slow; single visuals are fast alone | Too many visuals per page, each issuing its own query | Reduce visuals per page; move detail to drillthrough pages and page tooltips |
| One table visual dominates load time | Unfiltered table materialising millions of rows into memory at every refresh | Apply a Top N filter (set generously, e.g. 10,000); the user experience is unchanged and memory use drops sharply |
| Slow only in the Power BI service, fast in Desktop | Desktop environment differs — data volume, concurrency, storage mode, gateway, capacity, query caching | Re-test in the service; treat Desktop timings as a lower bound only |
| Slow every morning, fine by afternoon | Overnight background jobs smoothed over 24 hours are still consuming future capacity | Re-schedule refreshes; check the Overages tab for minutes-to-burndown |
| Users see a consistent ~20-second lag | Interactive delay stage: 10 to 60 minutes of future capacity consumed | Reduce interactive load or scale the SKU; a bounded overage limit prevents the delay |
| Queries rejected with "try again later" | Interactive rejection stage: 60 minutes to 24 hours of future capacity consumed | Scale up on demand; investigate the burst in the Metrics app timepoint drill-through |
| Scheduled refreshes silently stop running | Background rejection stage (beyond 24 hours), or surge protection rejecting background jobs | Confirm which in the Throttling tab; surge protection rejects background work by design to protect interactive users |
| Refresh fails with out-of-memory | Full refresh of a model too large for the SKU's per-model memory limit | Use incremental refresh; reduce refresh parallelism; remove high-cardinality columns; consider scale-out or a larger SKU |
| Refresh takes hours and grows every month | Full refresh of a fact table with no partitioning policy | Configure incremental refresh with RangeStart/RangeEnd; add Detect data changes to skip unchanged periods |
| Incremental refresh configured but still slow | Filter not folding to the source; the mashup engine is filtering locally after pulling everything | Verify query folding with step folding indicators; ensure the RangeStart/RangeEnd filter immediately follows the source step |
| Slicers are sluggish and show odd "with data" behaviour | Bi-directional cross-filter relationships expanding filter context across the model | Set relationships to single direction; enable bi-directional behaviour only inside a measure using CROSSFILTER |
| Model far larger than the source data | High-cardinality columns, unused columns, calculated columns computed at load | Drop unused columns; reduce cardinality; move calculated columns and tables upstream into ETL |
| DirectQuery report fails rather than slows | One-million-row return limit or the 225-second query timeout exceeded | Aggregate at source; add user-defined aggregations; reduce cardinality in TopN and measure filters |
| DirectQuery visuals each fire two source queries | Measure filters and TopN filters resolve in two passes | Pre-aggregate at source; avoid TopN over very high-cardinality columns |
| Direct Lake report suddenly slower with no change | Direct Lake on SQL fell back to DirectQuery — a SQL view, SQL-based RLS, or a guardrail exceeded | Optimise the Delta tables to fit SKU guardrails; materialise views; or use Direct Lake on OneLake, which does not fall back |
| Refresh error: "too many refreshes occurring concurrently" | Gateway concurrency limit of 30 reached | Add gateway cluster members or a second cluster; monitor with the gateway diagnostics template |
| Interactive reports degrade during refresh windows | Import refreshes and DirectQuery traffic share one gateway | Run separate gateways for Import and for DirectQuery/live connection models |
| Paginated report fails on export | Data volume limits exceeded — roughly 1,000,000 rows at 15–20 columns in the standard execution environment, double in the optimised one | Aggregate in the dataset query rather than in the report; export to CSV rather than Excel for data dumps |
Model shape: where the time actually goes
Report tuning has a ceiling set by model design. Four decisions set it.
Star schema is the target, not a preference. Dimensions filter and group; facts summarise. Relationship cardinality sets the role — the "one" side is always a dimension, the "many" always a fact. Every visual generates a query that filters, groups, and summarises, so dimensions-plus-facts is the shape the engine already queries. Avoid half-dimension, half-fact tables, and load facts at a consistent grain.
Cardinality is the memory bill. The columnar engine compresses repeated values well and unique values badly. A transaction ID or full timestamp in a fact table can outweigh every other column combined. Memory Analyzer ranks them. Split timestamps into date and time, drop unused surrogate keys, and remove columns nobody has put on a visual.
Bi-directional filters are a last resort. Microsoft's guidance is to minimise them — they degrade query performance and confuse slicer behaviour. Three cases are legitimate: one-to-one, which has no alternative; many-to-many via a bridging table; and dimension-to-dimension analysis through a fact table acting as a bridge. Even then, leave the relationship single-directional and switch filtering on inside the measure with CROSSFILTER, so the cost is paid only where needed.
Calculated columns are usually in the wrong place. A calculated column computes at load and is stored, consuming memory forever; a measure computes at query time and stores nothing. Where a value must be materialised, push it upstream into ETL or the warehouse. Microsoft's out-of-memory remediation guidance names exactly this as a primary mitigation.
Storage mode decision matrix
| Consider | Direct Lake on OneLake | Direct Lake on SQL | Import | DirectQuery |
|---|---|---|---|---|
| Query engine | VertiPaq | VertiPaq | VertiPaq | Federated to source |
| Licence required | Fabric capacity only | Fabric capacity only | Any Power BI or Fabric licence | Any Power BI or Fabric licence |
| "Refresh" means | Framing — metadata only, seconds | Framing — metadata only, seconds | Full data copy; minutes to hours | No refresh; every query hits the source |
| Falls back to DirectQuery | No — fails instead if guardrails exceeded | Yes — for SQL views, SQL RLS, guardrail breaches | n/a | n/a |
| Composite models | Yes | No | Yes | Yes |
| Calculated columns | Preview, unmaterialised | No | Yes | Yes |
| Hard limits to watch | Per-SKU rows, parquet files, row groups, model size | Same, plus fallback behaviour | Per-SKU model memory; refresh duration | 1M row return limit; 225-second timeout |
| Choose when | Large Delta data in OneLake, freshness matters, no silent fallback wanted | You need SQL endpoint security (RLS/CLS/OLS) and can accept fallback | Self-service agility, modest volumes, Power Query needed, no Fabric capacity | Source is authoritative, data must be live, volumes small per query |
| Do not choose when | You depend on SQL-endpoint RLS or non-materialised views | Silent performance regressions are unacceptable | Model would exceed SKU memory, or refresh exceeds the window | Concurrency is high or visuals numerous — each re-queries the source |
Direct Lake guardrails scale with the SKU. On F64: 5,000 parquet files per table, 5,000 row groups, 1,500 million rows, 25 GB memory. On F256: 6,000 million rows and 100 GB. Optimise the Delta tables before scaling the SKU — guardrails are per query except model size on disk, which is evaluated at model level and affects every query.
The capacity side
Report tuning reduces demand. Capacity mechanics determine what happens to what remains.
Bursting lets an operation temporarily consume more compute than the SKU provides, so users get fast results and a smaller capacity can run larger jobs. Smoothing then averages that consumption forward — interactive across five to 64 minutes, background across a full 24 hours. Together they make a capacity self-managing under spikes, and they are why cause and effect are separated in time: the refresh that broke this morning ran last night.
Throttling starts only once smoothed usage has consumed all available future capacity. The stages are published and worth memorising:
| Future capacity consumed | Stage | What users experience |
|---|---|---|
| Up to 10 minutes | Overage protection | Nothing — this is designed headroom |
| 10 to 60 minutes | Interactive delay | New interactive operations delayed 20 seconds at submission |
| 60 minutes to 24 hours | Interactive rejection | User-requested queries rejected; background jobs still run |
| Beyond 24 hours | Background rejection | All requests rejected until consumed CU are paid off |
Two workload exceptions matter. Real-Time Intelligence skips the 20-second delay stage and only throttles at the rejection phase, because a delayed real-time query is a broken one. Almost all Warehouse operations are classified as background so 24-hour smoothing absorbs their peaks — meaning a slow warehouse query may never appear in your interactive throttling charts.
Surge protection limits background compute when 24-hour usage is high, prioritising user-facing queries. Read the tradeoff plainly: it rejects background jobs, those jobs may need re-running, and many operations you think of as interactive — SQL queries among them — are classified as background and can be rejected too. Microsoft is explicit that it is not a substitute for correct sizing.
Scaling down is measured, not guessed. Microsoft's worked example: to move from F128 to F64 safely, smoothed usage on the F128 should sit below 40%, because it roughly doubles to 80% at half the size. Apply the same arithmetic in reverse before scaling up.
What changed in 2026
- Capacity overage (preview) changed the throttling conversation. When enabled, capacity does not throttle until the admin-set limit is exceeded; the Metrics app reports "no interactive delay (capacity overage billed)." Azure bills overage at three times pay-as-you-go rates, so hold the rolling 24-hour limit below one-third of daily CU hours.
- Autoscale is gone on F SKUs. Replacements: on-demand resizing in the Azure portal, pause and resume, and bounded capacity overage.
- Direct Lake split into two modes with different failure behaviour. Direct Lake on OneLake fails when guardrails are exceeded; Direct Lake on SQL falls back to DirectQuery, producing silent performance regressions instead of errors. That distinction changes how you monitor.
- Best Practice Analyzer and Memory Analyzer moved into Fabric notebooks, so model audits run against published models on a schedule rather than only on a modeller's laptop.
- Paginated report data-volume breaches now fail by design rather than degrading, surfacing problems earlier but breaking jobs that previously limped through.
- Sizing evidence matured: the Fabric SKU Estimator plus 30–45 days of Capacity Metrics baseline is now the expected basis for a sizing decision.
Where EPC Group fits
EPC Group has tuned Microsoft BI estates since 1997 — 29 years, six Microsoft Solutions Partner designations, 11,000+ engagements, six consecutive G2 Leader quarters. Errin O'Connor, Founder and Chief AI Architect, is a 4× Microsoft Press bestselling author. Multiple models. One truth.
Related reading: Power BI consulting services · Power BI gateway · Power BI Premium · Premium vs Premium Per User · Power BI cost and licensing guide · Running totals and cumulative sums in DAX · Power BI GROUPBY · Power BI conditional formatting · Microsoft Fabric consulting services · Fabric consulting guide · Microsoft Fabric vs Databricks · Snowflake to Fabric migration · Epic and Cerner Power BI integration · Data governance consulting · Power BI consulting in Houston · Power BI consulting in Phoenix · Microsoft Frontier Company · EPC Group.
Run the trace on your slowest report
Send the report name, the SKU it runs on, and a Performance Analyzer export. EPC Group returns the probe at which it fails, the cause, and the fix — before anyone proposes a bigger capacity. Start at Power BI consulting services.
Frequently asked questions
What is the first thing to check on a slow Power BI report?
Open the report in Power BI Desktop and run Performance Analyzer. It separates DAX query duration from visual display duration for every element on the page. If display time dominates, the problem is report design. If query time dominates, the problem is the DAX or the model. Checking capacity first wastes time on most engagements.
Why is my report slow in the Power BI service but fast in Desktop?
Performance Analyzer runs on the modeller's machine, so it does not reflect service conditions. Data volume, concurrent user count, table storage modes, gateway involvement, whether a capacity is involved, query caching, and query parallelisation all differ. Treat Desktop timings as a best case and re-measure in the service.
Does capacity utilisation over 100% mean my reports are throttled?
No. Fabric uses bursting deliberately, so exceeding 100% utilisation is expected and often harmless. Throttling only begins once smoothed usage has consumed all available future capacity. Use the Throttling tab in the Fabric Capacity Metrics app, not the Utilization tab, to determine whether users are being affected.
What are the Fabric throttling stages?
There are four. Up to 10 minutes of future capacity consumed is overage protection with no user impact. From 10 to 60 minutes, new interactive operations are delayed 20 seconds. From 60 minutes to 24 hours, interactive operations are rejected while background jobs continue. Beyond 24 hours, all requests are rejected until the consumed capacity units are paid off.
Why is my report slow in the morning when nothing changed?
Background operations are smoothed over a 24-hour window, so an overnight refresh or Spark job is still consuming future capacity hours later. Interactive operations smooth over five to 64 minutes. Check the Overages tab in the Capacity Metrics app for cumulative carryforward and minutes-to-burndown, then reschedule the offending jobs.
Should I use bi-directional relationships in Power BI?
Minimise them. They degrade query performance and can produce confusing slicer behaviour. Three cases justify them: one-to-one relationships, many-to-many through a bridging table, and dimension-to-dimension analysis. Even then, prefer leaving the relationship single-directional and enabling both directions inside a specific measure using the CROSSFILTER function.
Should I use calculated columns or measures?
Prefer measures. A calculated column is computed at load and stored in the model, consuming memory permanently. A measure is computed at query time and stores nothing. Where a value must be materialised, push it upstream into ETL or the data warehouse. Microsoft names moving calculated tables and columns to the data source as a primary fix for out-of-memory refresh failures.
When should I use Direct Lake instead of Import?
Use Direct Lake when large volumes of Delta-format data already sit in OneLake and copying them into an Import model is impractical. Direct Lake refresh copies metadata only, which takes seconds rather than the minutes or hours a full Import refresh needs. Import remains the right choice for self-service work, Power Query transformation, and models without Fabric capacity.
Why did my Direct Lake report get slower without any change?
Direct Lake on SQL endpoints falls back to DirectQuery when it cannot load data directly — for example against a non-materialised SQL view, with SQL-based row-level security, or when a per-SKU guardrail is exceeded. Queries still return results but perform like DirectQuery. Direct Lake on OneLake does not fall back; it fails instead.
What are the DirectQuery limits I should design around?
DirectQuery returns a maximum of one million rows and enforces a 225-second query timeout. Measure filters and TopN filters cause two queries to be sent to the source rather than one, and TopN retrieves all candidate values before ranking. Design aggregations at the source and avoid TopN over very high-cardinality columns.
How do I fix a Power BI refresh that runs out of memory?
Refresh only the partitions or tables that changed by configuring incremental refresh. Reduce refresh parallelism when processing multiple partitions of the same model. Reduce model complexity by removing unnecessary columns and rows, cutting high-cardinality columns, and moving calculated tables and columns upstream. Then consider semantic model scale-out or a larger F SKU.
Sources and verification
- Microsoft Learn — Troubleshoot report performance in Power BI (triage flowchart, six terminators)
- Microsoft Learn — Monitor report performance in Power BI (Performance Analyzer, Query Diagnostics, SQL Server Profiler)
- Microsoft Learn — Optimization guide for Power BI (visual count, Top N filter guidance, custom visual evaluation)
- Microsoft Learn — The Fabric throttling policy (bursting, smoothing windows, four throttle stages, workload exceptions)
- Microsoft Learn — Compute page in the Fabric Capacity Metrics app (Utilization, Throttling, Overages tabs; 30-second timepoints)
- Microsoft Learn — Plan your capacity size (CU per SKU, 30-second evaluation period)
- Microsoft Learn — Microsoft Fabric capacity planning guide: manage growth and governance (surge protection tradeoffs, F128→F64 scale-down arithmetic)
- Microsoft Learn — Understand star schema and the importance for Power BI
- Microsoft Learn — Bi-directional relationship guidance (minimise use; CROSSFILTER alternative)
- Microsoft Learn — Direct Lake overview (storage mode comparison, per-SKU guardrails, DirectQuery fallback behaviour)
- Microsoft Learn — Develop Direct Lake semantic models (choosing Direct Lake on OneLake vs on SQL)
- Microsoft Learn — DirectQuery model guidance in Power BI Desktop (measure filters, TopN filters, source optimisation)
- Microsoft Learn — Data refresh in Power BI (1M row limit, 225-second timeout, gateway separation, refresh scheduling)
- Microsoft Learn — Troubleshoot refresh scenarios (out-of-memory mitigations, scale-out, parallelism)
- Microsoft Learn — Configure incremental refresh and real-time data for Power BI semantic models
- Microsoft Learn — Monitor and optimize on-premises data gateway performance (diagnostics template, spool storage, streaming)
- Microsoft Learn — Troubleshoot the on-premises data gateway (30 concurrent refresh limit)
- Microsoft Learn — Guidance for deploying a data gateway for the Power BI service (location, latency, sizing)
- Microsoft Learn — External tools in Power BI Desktop (DAX Studio, Tabular Editor, ALM Toolkit)
- Microsoft Learn — Use notebooks with a semantic model (Best Practice Analyzer 60+ rules, Memory Analyzer)
- Microsoft Learn — Power BI implementation planning: data-level auditing (VertiPaq Analyzer, .vpax export, BPA rules on GitHub)
- Microsoft Learn — Performance and scalability considerations for paginated reports (1,000,000 rows at 15–20 columns; doubled in optimised environment)
- Microsoft Learn — Capacity and SKUs in Power BI embedded analytics (per-SKU semantic model memory, 25 GB on F64)
- Microsoft Learn — Capacity overage (preview) in Microsoft Fabric (3× pay-as-you-go billing, rolling 24-hour limit)
