Microsoft Fabric  

Stop killing your Fabric Capacity now !

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 :

  • One shared semantic model: Employees Performance Model

  • Three reports using it:

    • Global Employee Performance

    • Regional Employee Performance

    • District Employee Performance

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:

  • Long text descriptions

  • Phone numbers

  • Status descriptions

  • Dates not used in visuals

  • Legacy attributes

  • Technical fields

  • Duplicate business keys

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:

  • Employeer SIM card number

  • Insurance number

  • Driving license number

  • Birth certificate number

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:

  • A few unused columns

  • A few heavy measures

  • A few overloaded pages

  • A few bad relationships

  • A few full refreshes

  • A few jobs running at the wrong time

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?