![Microsoft Fabric: Data Analytics and Business Intelligence]()
Microsoft Fabric capacity is powerful, but it is not unlimited magic.
Many teams buy Fabric capacity expecting it to behave like an infinite pool of compute. Then, after a few months, the same questions start appearing:
“Why is the report slow?”
“Why is the capacity throttled?”
“Why are refreshes delayed?”
“Why is everything slow even though we increased the SKU?”
In many cases, the problem is not only the capacity size. The real problem is how the capacity is being used.
This article I will exaplain common patterns that silently kill Fabric capacity, using examples based on some scenarios I worked on.
Fabric capacity is shared, not dedicated to 1 report
A Fabric capacity is a shared compute environment.
It can be used by:
Power BI semantic models
Power BI reports
Dataflows Gen2
Pipelines
Lakehouses
Warehouses
Notebooks
Spark jobs
Direct Lake models
Scheduled refreshes
Interactive users
So when one poorly optimized report or refresh consumes too much compute, it can impact everyone else using the same capacity.
This is why capacity performance should not be treated only as an infrastructure topic. It is also a modeling, DAX, data engineering and governance topic.
One shared semantic model, 3 reports, same problem everywhere
Imagine this situation :
At first, users complain that one report is slow. But after investigation, the issue is not isolated to one report.
The problem is inside the shared semantic model.
Because all 3 reports use the same model, the same issues are inherited everywhere:
Heavy fact table
Unused columns
Expensive measures
Complex relationships
High-cardinality text keys
Many-to-many relationships
Repeated DAX patterns
Too many visuals on some pages
This is a classic capacity killer.
When the shared model is inefficient, every report built on top of it becomes inefficient too.
Keeping every column "Just in Case"
One common mistake is keeping too many columns in the semantic model.
For example, an employee table may contain 50 columns, but only 4 are actually used in reports.
The unused columns may include:
Even if users never see these columns, they still increase the model size.
This affects:
Memory usage
Refresh duration
Compression efficiency
Dataset loading time
Capacity pressure
A semantic model should not be a data dump. It should contain only what is needed for reporting and analysis.
Bad practice
Loading everything from the source:
SELECT *
FROM EmployeeMaster
Better practice
Select only required columns:
SELECT
EmployeeKey,
EmployeeName,
EmployeeCategory,
EmployeeHierarchyCode
FROM EmployeeMaster
The goal is not to remove data blindly. The goal is to validate what is really used and remove what is not required.
Example 3: High cardinality text keys everywhere
Another silent killer is the use of long text columns as relationship keys.
For example:
These columns often have high cardinality, meaning many unique values.
When high cardinality text columns are used in relationships, they increase memory usage and can slow down filtering.
Risky model pattern
FactPerformance[EmployeeNumber] --> Employee[EmployeeNumber]
Where EmployeeNumber is a long text field.
Better model pattern
Use an integer surrogate key when possible:
FactPerformance[EmployeeKey] --> Employee[EmployeeKey]
This improves compression and relationship performance.
Business keys are still important but they do not always need to be the relationship keys used internally by the model.
Example 4: Many to many relationships with both direction filtering
Many to many relationships are sometimes necessary, but they are also dangerous when used without control.
A risky pattern looks like this:
Table A <--> Table B
Many-to-many
Both-direction filtering
This can create ambiguous filter paths, unexpected results and expensive query plans.
For example:
Employee--> Fact Table
Employee--> Departments Employees Mapping Table --> Fact Table
Now the same product filter can reach the fact table through more than one path.
This creates relationship ambiguity and can make DAX queries harder for the engine to optimize.
Symptoms
Report pages become slow
Slicers behave unexpectedly
Measures return confusing results
Capacity usage increases
Query plans become complex
Better approach
Prefer a clean star schema:
Dimension tables --> Fact table
Use single direction filtering where possible.
Avoid many to -many relationships unless there is a clear business reason and the behavior is tested carefully.
Example 5: Expensive DAX repeated across many measures
DAX can kill your capacity when measures are written in a way that forces the engine to scan large tables repeatedly.
A common pattern is:
Bad Measure =
COUNTROWS (
FILTER (
FactTable,
FactTable[Status] = "Active"
)
)
This works but it can be inefficient on large fact tables.
A better version is usually:
Better Measure =
CALCULATE (
COUNTROWS ( FactTable ),
FactTable[Status] = "Active"
)
The second version allows the engine to apply filters more efficiently.
Another common issue is repeating the same logic across many measures.
For example:
Current Quarter Sales =
CALCULATE (
[Sales],
FILTER (
Calendar,
Calendar[Quarter] = FORMAT ( TODAY(), "YYYY\QQ" )
)
)
If this pattern is repeated in many measures, the model becomes harder to maintain and more expensive to query.
Better options include:
Pre calculating period flags in the date table
Using proper date intelligence
Avoiding repeated TODAY() and FORMAT() logic
Creating reusable base measures
Example 6: FORMAT() used for logic instead of display
FORMAT() is useful for display but it converts values to text.
That can hurt performance and sorting.
Risky example
Quarter Label =
FORMAT ( Calendar[Date], "YYYY-Q" )
If this value is then used for filtering, sorting or joining logic, it can become inefficient.
Better approach
Keep numeric or date logic separate from display labels.
Example:
QuarterSort =
YEAR ( Calendar[Date] ) * 10 + QUARTER ( Calendar[Date] )
And use a separate label only for display:
QuarterLabel =
"Q" & QUARTER ( Calendar[Date] ) & " " & YEAR ( Calendar[Date] )
This keeps sorting and filtering more efficient.
Example 7: Too many visuals on one page
Sometimes the model is not the only issue. The report page itself can overload the capacity.
A page with many visuals means many queries.
For example, one page may contain:
8 cards
4 matrix visuals
3 charts
6 slicers
Dynamic titles
Conditional formatting
Tooltip pages
Field parameters
Each visual can generate one or more DAX queries.
When many users open the same page at the same time, the capacity can be hit hard.
Common symptoms
Page takes long to load
Visuals appear one by one
Slicers are slow
Users refresh repeatedly, making it worse
Capacity spikes during business hours
Better approach
Reduce the visual load:
Split heavy pages into multiple pages
Remove duplicate visuals
Avoid unnecessary matrix visuals
Use drillthrough instead of showing everything at once
Limit expensive conditional formatting
Use Performance Analyzer to identify slow visuals
A beautiful report that destroys the capacity is not a successful report.
Example 8: Refreshing Everything Every Time
Another way to kill capacity is refreshing full historical data every day when only recent data changes.
For example:
Fact table rows: 50 million
Daily changed rows: 100,000
Refresh strategy: Full refresh
This is wasteful.
If historical data does not change you can use incremental refresh or partitioning where possible.
Better approach
Refresh recent periods only
Keep historical partitions static
Push filters to the source
Make sure query folding works
Avoid transformations that force full scans
For Power BI semantic models, incremental refresh can significantly reduce load when configured correctly.
For Fabric pipelines and Lakehouse patterns, you can always use delta based loading instead of repeatedly reprocessing everything.
Example 9: Running heavy jobs during peak reporting hours
Capacity problems are often caused by bad scheduling.
For example:
08:30 - Dataset refresh starts
09:00 - Business users open reports
09:05 - Notebook job starts
09:10 - Dataflow refresh starts
09:15 - Warehouse query starts
All of this happens on the same capacity.
Then users say: “Power BI is slow.”
But the real issue is workload collision.
Better approach
Separate workloads by schedule:
Run heavy data engineering jobs outside reporting peak hours
Avoid overlapping large refreshes
Stagger semantic model refreshes
Monitor capacity usage by time window
Use dedicated capacities for critical workloads when needed
Capacity planning is not only about SKU size. It is also about workload timing.
Example 10: Treating capacity upgrade as the 1st solution
Increasing the capacity SKU can help but it should not be the first solution.
If the model is inefficient, a bigger capacity only hides the problem temporarily.
Bad model design on a small capacity becomes expensive bad model design on a bigger capacity.
Before upgrading, check:
Is the semantic model optimized?
Are unused columns removed?
Are relationships clean?
Are DAX measures efficient?
Are refreshes incremental?
Are report pages overloaded?
Are workloads scheduled properly?
Are users creating unnecessary duplicate reports?
Are Direct Lake models falling back unexpectedly?
Are long-running operations monitored?
Scaling should come after optimization, not before.
The main lesson
Fabric capacity is not killed by one single bad decision.
It is usually killed by a combination of small issues:
Together, they create a performance problem that affects everyone.
The solution is not only to buy more capacity.
The solution is to build better models, write better DAX, design better reports, schedule workloads properly and monitor usage continuously.
A healthy Fabric capacity starts with responsible design.
Before asking “Do we need a bigger capacity?”, ask:
Are we using the current capacity correctly?