There's a particular kind of Power BI report that starts fast and dies slowly. It works beautifully on day one, and then, month by month, the refresh gets longer — five minutes, then fifteen, then the dreaded moment when it overruns the window, times out, and someone opens a ticket that says "the dashboard is broken" when the dashboard is fine and the refresh is drowning. Nine times out of ten the cause is the same, and it's almost comically wasteful: every single refresh is reloading the entire history of the data — five years of transactions that have not changed and will never change again — just to pick up yesterday's handful of new rows. Incremental refresh is the fix, and understanding it properly is one of the highest-leverage things a Power BI developer can learn.

The idea is simple enough to state in a sentence: only reload the data that might have changed, and leave the settled history alone. But the implementation has a few moving parts that trip people up, so let me walk it properly, because the details are where refreshes get quietly broken.

The core pattern

Incremental refresh works by partitioning your table along a date column and treating recent partitions differently from old ones. You tell Power BI two things: how much history to store, and how much of the recent past to refresh on each run. A typical configuration might store five years of data but only refresh the last ten days — so every refresh reloads a week and a bit of recent rows and leaves the other four-plus years exactly as they are, untouched and instant.

The mechanism rests on two special parameters, RangeStart and RangeEnd, which you define in Power Query. These aren't ordinary parameters — Power BI recognises them by name and uses them to slice the data into partitions. Your job is to filter your source query on a date column using these two parameters:

= Table.SelectRows(Source, each [OrderDate] >= RangeStart and [OrderDate] < RangeEnd)

Get that filter right and Power BI does the rest: it generates the partitions, refreshes only the recent ones, and — crucially — pushes that date filter down to the source so the database only ever returns the slice you asked for.

Query folding is the whole game

Here's the part people miss, and it's the difference between incremental refresh working and merely pretending to work: the date filter has to fold. Query folding is Power Query's ability to translate your transformations back into native queries that run at the source. When folding works, that RangeStart/RangeEnd filter becomes a WHERE clause the database executes, and only the relevant rows ever cross the wire. When folding breaks — because you did something earlier in the query that can't be translated, like certain custom columns or steps that force everything into memory first — Power BI ends up pulling the whole table anyway and filtering it locally, which defeats the entire purpose. You've configured incremental refresh and gained nothing, and you won't notice until you wonder why the "fixed" refresh is still slow.

So the discipline is: keep the query foldable up to and including the date filter. Do your unfoldable cleverness after the partitioning filter, or better, push it upstream into the source or a view. Test that folding survives — right-click the step and check whether "View Native Query" is available; if it's greyed out, folding has broken above that point and your incremental refresh is a decoration.

Where it pays off, and where it bites

The payoff is enormous for the right table: large fact tables with an append-mostly pattern, where history is settled and only recent data changes. A refresh that took twenty minutes reloading everything can drop to under a minute reloading only the tail. The compute you're not spending is real money on a capacity, and the refresh window you're not blowing is a 3 a.m. alert you'll never get.

The bites are worth knowing before you're bitten:

  • Late-arriving data. If rows can change after they've fallen outside your refresh window — a correction to a three-month-old order — a naive incremental refresh won't pick them up, because it's not looking there anymore. You need a wider refresh window, or a periodic full reload, to catch them.
  • The first refresh is still big. Incremental refresh partitions on the next refresh after publishing; the initial load still brings in the full history once. Plan for that first heavy refresh.
  • It wants a real date column. The pattern lives or dies on a reliable date to partition by. Tables without a clean temporal key aren't candidates.

The habit worth building

Incremental refresh is one of those techniques that, once it's in your hands, changes how you build. You stop treating "reload everything" as the default and start asking, for every sizeable table, what actually changes here, and how little can I get away with reloading? That question is the whole of efficient data work in miniature — not just in Power BI, but everywhere. The slow way and the fast way often differ by nothing more than noticing that most of your data already arrived, already settled, and does not need to be fetched again. Do the small amount of work to set the pattern up correctly, respect query folding, and you retire a whole category of the "why is this so slow" problem — cheaply, permanently, and without anyone having to open a ticket.