When hundreds of users hammer a single Power BI dataset simultaneously, no amount of DAX optimization saves you — you need architectural separation of read and write operations. This deep-dive lesson teaches you to implement Query Scale-Out with read-only replicas from capacity sizing through monitoring, including the consistency window behaviors that most tutorials skip entirely.

Picture this: your enterprise Power BI environment is humming along on a Monday morning. Finance is running their end-of-month variance reports, the sales team is hammering the regional pipeline dashboards, and the executive team has scheduled their weekly KPI review — all at the same time, all hitting the same 40 GB semantic model that took three hours to refresh overnight. By 9:15 AM, you're getting Slack messages from department heads about sluggish dashboards, and your Power BI Premium capacity is pegged at 100% CPU. The refresh you need to kick off for the updated actuals data is sitting in a queue, waiting for resources to free up. You've been here before. You know the fix is not simply throwing more capacity at the problem.
The real issue is architectural: you have a single processing node absorbing every read query, every DAX calculation, and every data refresh simultaneously. Power BI's Query Scale-Out feature — available on Premium Per Capacity (P-SKU), Premium Per User (PPU), and the newer Fabric F-SKU capacities — directly addresses this by introducing read-only replicas of your semantic model. Instead of one dataset engine handling everything, query scale-out creates multiple synchronized copies of your model across separate processing nodes, routing interactive report queries to those replicas while the primary dataset handles data refreshes and write operations. The result is a fundamental separation of concerns at the infrastructure level.
By the end of this lesson, you will understand not just how to flip on the feature, but the underlying architecture that makes it work, the scenarios where it shines (and where it doesn't), and the operational discipline required to run it reliably at scale. You'll walk away with the knowledge to design, implement, and troubleshoot a query scale-out deployment for a production enterprise environment.
What you'll learn:
Before working through this lesson, you should be comfortable with:
If you're newer to Premium capacity administration, work through the foundational Premium capacity lessons in this learning path first.
Before you configure anything, you need a precise mental model of how this feature works. The marketing description — "distribute queries across replicas" — glosses over the details that determine whether your implementation succeeds or causes subtle data consistency bugs in production.
Power BI semantic models run on the Analysis Services Vertipaq in-memory engine. Vertipaq is a columnar, in-memory store optimized for DAX query patterns. A critical characteristic of Vertipaq is that it operates on a single-writer, multiple-reader model. When a dataset refresh runs, Vertipaq must reconstruct column stores, recalculate hierarchies, and commit new data — operations that acquire exclusive write access to the in-memory structures. During this process, concurrent read queries either wait or are served from a snapshot of the previous model state, depending on how the refresh is configured.
In a standard Premium dataset without scale-out, all of this happens on a single processing node. Read queries from interactive reports and background queries from scheduled reports compete directly with refresh operations for CPU and memory resources. When you have a 40 GB model with aggressive refresh schedules and hundreds of concurrent users, this contention is the root cause of your performance problems.
When you enable Query Scale-Out on a dataset, the Power BI service creates one or more read-only replicas of that dataset on separate nodes within your Premium capacity. These are not separate logical datasets — they are engine-level copies of the same semantic model, managed by the Power BI service automatically.
The synchronization mechanism works as follows:
.abf backup format) is propagated to each read-only replica node.This is the architectural reality that creates the consistency window — the period between when data is refreshed on the primary and when all replicas reflect that updated state. For most enterprise reporting scenarios, this window is seconds to a few minutes. But for certain use cases — near-real-time operational dashboards, financial reporting where users cross-reference live transactional data — you must account for it explicitly.
The Power BI service includes a query router component that sits in front of your dataset and directs incoming connections. Here's how routing decisions are made:
DataSourceType=ReadWrite in the XMLA connection string bypass replicas and hit the primary.This routing happens transparently for most Power BI report connections. The browser-based Power BI report viewer doesn't know or care which node it's hitting — it just sends DAX queries through the Power BI service, which routes them appropriately. The complexity emerges when you have direct XMLA connections from tools like Excel, third-party BI tools, or custom applications, and you need explicit control over which node they target.
Query Scale-Out is not enabled by default. Configuration happens in layers: capacity-level settings establish the maximum number of replicas available, and dataset-level settings control whether and how a specific dataset uses them. Let's walk through both.
Sign in to the Power BI Admin Portal at app.powerbi.com/admin-portal. Navigate to Capacity settings and select your Premium capacity (P1, P2, F64, or larger — query scale-out requires at minimum P1 or F64). Under the Power BI workloads section, find the Datasets workload configuration.
You'll see a setting labeled Max Memory for datasets and, critically, the Query Scale-Out setting. The Query Scale-Out toggle at the capacity level is a prerequisite — if it's off here, no dataset in this capacity can use replicas regardless of dataset-level settings.
What the admin portal doesn't tell you — and what matters a great deal operationally — is that enabling Query Scale-Out at the capacity level reserves a portion of your capacity's total memory for replica hosting. On a P1 capacity (25 GB total dataset memory), enabling two replicas effectively divides your available memory three ways: primary dataset plus two replicas. A 40 GB model won't fit on a P1 with replicas enabled for that dataset, which means capacity sizing must account for the replica memory footprint before you enable this feature.
The calculation you should do before enabling scale-out:
Required capacity memory = (dataset_size_in_memory × (1 + replica_count)) + headroom_for_other_datasets
For a dataset that loads to approximately 35 GB in memory with two replicas, you need at minimum 105 GB of dataset memory capacity — a P3 or F256. This is the most common reason enterprises turn on scale-out and immediately experience out-of-memory errors.
Warning: The in-memory size of a dataset is not the same as its .pbix file size or the size of the source data. A 5 GB .pbix file might expand to 20 GB in memory after Vertipaq loads and decompresses its column stores. Always profile your dataset's actual in-memory footprint using the
System.MemoryDMV via XMLA before calculating replica capacity requirements.
Connect to your dataset via SSMS or Tabular Editor using the XMLA endpoint (format: powerbi://api.powerbi.com/v1.0/myorg/YourWorkspaceName). Run the following DMV query:
SELECT
DIMENSION_NAME,
ATTRIBUTE_NAME,
DICTIONARY_SIZE,
USED_SIZE
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNS
ORDER BY USED_SIZE DESC
For total model memory consumption:
SELECT
DATABASE_NAME,
[OBJECT_TYPE],
SUM(USED_SIZE) as TotalUsedSize,
SUM(DICTIONARY_SIZE) as TotalDictionarySize
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNS
WHERE DIMENSION_NAME IS NOT NULL
GROUP BY DATABASE_NAME, [OBJECT_TYPE]
This gives you the actual Vertipaq memory footprint broken down by column store data versus dictionaries. For capacity planning, use the TotalUsedSize figure. Convert from bytes to gigabytes (divide by 1,073,741,824) to compare against your capacity limits.
Once the capacity is correctly sized and scale-out is enabled at the capacity level, you configure individual datasets. Not every dataset needs scale-out — applying it indiscriminately wastes capacity memory. Target it at the datasets that are both large (memory-intensive to load) and heavily queried concurrently.
In the Power BI service, navigate to your workspace and find the semantic model (dataset) you want to configure. Select the Settings option from the dataset's context menu (the three-dot menu). In the settings pane, navigate to the Scale-out section.
You'll see two key controls here:
The service doesn't always create the maximum number of replicas immediately. It scales replica count based on available capacity resources. In practice, on a properly sized capacity, you'll reach the maximum replica count within a few minutes of enabling the feature.
Tip: The Power BI service automatically manages replica lifecycle — creating them when resources are available and removing them when capacity is constrained. This elastic behavior is by design, but it means you cannot guarantee a specific number of active replicas at any given time. Design your monitoring to alert on replica count dropping below your expected minimum.
For production environments, you should manage scale-out configuration through the REST API or PowerShell rather than the UI. This enables configuration as code, integration with deployment pipelines, and auditability. Here's the REST API approach:
PATCH https://api.powerbi.com/v1.0/myorg/groups/{workspaceId}/datasets/{datasetId}
Content-Type: application/json
Authorization: Bearer {access_token}
{
"queryScaleOutSettings": {
"autoSyncReadOnlyReplicas": true,
"maxReadOnlyReplicas": 3
}
}
The autoSyncReadOnlyReplicas property is worth understanding carefully. When set to true (the default), the service automatically synchronizes replicas after each successful refresh. When set to false, replicas are not automatically updated after refreshes — you must trigger synchronization manually via a separate API call. This manual synchronization mode is actually useful in specific scenarios, which we'll cover shortly.
If you're managing scale-out across dozens of datasets, PowerShell with the MicrosoftPowerBIMgmt module is more practical than manual REST calls:
# Install the module if needed
Install-Module -Name MicrosoftPowerBIMgmt -Scope CurrentUser
# Connect to Power BI
Connect-PowerBIServiceAccount
# Function to enable scale-out on a dataset
function Enable-DatasetScaleOut {
param(
[string]$WorkspaceId,
[string]$DatasetId,
[int]$MaxReplicas = 3,
[bool]$AutoSync = $true
)
$body = @{
queryScaleOutSettings = @{
autoSyncReadOnlyReplicas = $AutoSync
maxReadOnlyReplicas = $MaxReplicas
}
} | ConvertTo-Json
$url = "https://api.powerbi.com/v1.0/myorg/groups/$WorkspaceId/datasets/$DatasetId"
Invoke-PowerBIRestMethod -Url $url -Method Patch -Body $body -ContentType "application/json"
Write-Host "Scale-out configured for dataset $DatasetId with $MaxReplicas replicas"
}
# Apply to your target dataset
Enable-DatasetScaleOut `
-WorkspaceId "a7f2e3b1-4c9d-4e2a-8f1b-3c7e9d0a1b2c" `
-DatasetId "b3e1a9c2-7d4f-4b8e-9c2a-5f3d7b0e1c4a" `
-MaxReplicas 3 `
-AutoSync $true
This is where most enterprise implementations either succeed or develop subtle, hard-to-diagnose bugs. Query scale-out introduces a form of eventual consistency into your reporting environment. After a refresh completes on the primary node, there is a propagation period before all replicas reflect the updated data. During this window, different users may see different data depending on which replica serves their query.
In practice, the propagation window for most enterprise datasets on a properly resourced capacity ranges from 30 seconds to 5 minutes. The factors that influence this:
You can query the current synchronization state of your replicas via the REST API:
GET https://api.powerbi.com/v1.0/myorg/groups/{workspaceId}/datasets/{datasetId}/queryScaleOut/syncStatus
Authorization: Bearer {access_token}
The response provides per-replica synchronization timestamps:
{
"commitVersion": "2024-01-15T06:47:23.000Z",
"commitTimestamp": "2024-01-15T06:47:23.000Z",
"minActiveReadVersion": "2024-01-15T06:45:12.000Z",
"syncStartTime": "2024-01-15T06:47:25.000Z",
"syncEndTime": "2024-01-15T06:47:51.000Z",
"replicasSyncState": "Succeeded"
}
The commitTimestamp tells you when the primary completed its refresh. The syncEndTime tells you when the last replica finished loading. The difference is your actual consistency window for that refresh cycle.
Here's a sophisticated pattern used by enterprises with strict data consistency requirements — for example, month-end financial close reporting where all users must see the same data simultaneously, not a staggered view during the propagation window.
Set autoSyncReadOnlyReplicas to false during the critical reporting window. After your primary refresh completes and your finance team has validated the data via a direct primary connection, you trigger synchronization explicitly:
POST https://api.powerbi.com/v1.0/myorg/groups/{workspaceId}/datasets/{datasetId}/queryScaleOut/sync
Authorization: Bearer {access_token}
This initiates replica synchronization on demand. You can poll the sync status endpoint until replicasSyncState shows Succeeded, then send a notification to finance that all dashboards are showing validated, consistent data.
The PowerShell implementation of this pattern:
function Invoke-ControlledDatasetSync {
param(
[string]$WorkspaceId,
[string]$DatasetId,
[int]$TimeoutSeconds = 600
)
$syncUrl = "https://api.powerbi.com/v1.0/myorg/groups/$WorkspaceId/datasets/$DatasetId/queryScaleOut/sync"
$statusUrl = "https://api.powerbi.com/v1.0/myorg/groups/$WorkspaceId/datasets/$DatasetId/queryScaleOut/syncStatus"
# Trigger synchronization
Write-Host "Initiating replica synchronization at $(Get-Date -Format 'HH:mm:ss')..."
Invoke-PowerBIRestMethod -Url $syncUrl -Method Post
# Poll until complete or timeout
$elapsed = 0
$pollInterval = 15
while ($elapsed -lt $TimeoutSeconds) {
Start-Sleep -Seconds $pollInterval
$elapsed += $pollInterval
$status = Invoke-PowerBIRestMethod -Url $statusUrl -Method Get | ConvertFrom-Json
Write-Host "[$elapsed s] Sync state: $($status.replicasSyncState)"
if ($status.replicasSyncState -eq "Succeeded") {
Write-Host "Synchronization completed at $(Get-Date -Format 'HH:mm:ss')"
Write-Host "Consistency achieved as of: $($status.commitTimestamp)"
return $true
}
if ($status.replicasSyncState -eq "Failed") {
Write-Error "Replica synchronization failed. Check capacity health."
return $false
}
}
Write-Error "Synchronization timed out after $TimeoutSeconds seconds."
return $false
}
Warning: During the period when
autoSyncReadOnlyReplicasisfalseand you haven't triggered a manual sync, your replicas continue serving queries from the previous refresh's data. This is intentional in the controlled-sync pattern but can be alarming if you haven't designed for it. Document this behavior explicitly in your runbooks.
For advanced scenarios, you need explicit control over which node a specific connection targets. Power BI's XMLA endpoint exposes this through connection string parameters.
When you need a tool or application to always hit the primary dataset — for administrative work, model exploration with guaranteed latest data, or write operations — append DataSourceType=ReadWrite to the XMLA connection string:
Data Source=powerbi://api.powerbi.com/v1.0/myorg/YourWorkspaceName;
Initial Catalog=YourDatasetName;
DataSourceType=ReadWrite
In SSMS, this goes in the Server field when connecting. In Tabular Editor, it's configurable in the connection dialog.
To explicitly target the replica pool (useful for testing that replicas are serving correctly, or for tools that you want to ensure don't impact primary node resources):
Data Source=powerbi://api.powerbi.com/v1.0/myorg/YourWorkspaceName;
Initial Catalog=YourDatasetName;
DataSourceType=ReadOnly
Power BI Desktop files that use a live connection to a Premium dataset always use the default routing — which means they'll be directed to read-only replicas when scale-out is active. This is the correct behavior for report development and sharing. However, if a report developer needs to validate against the primary's latest data during active development, they can temporarily modify the connection in Desktop's data source settings to include ReadWrite in the connection string — though this is a manual step and should be documented in your development workflow.
Paginated reports (Power BI Report Builder) connect to datasets via the XMLA endpoint and can specify connection string properties. For paginated reports that run large, complex DAX queries — subscription-based delivery reports, large tabular exports — routing them to read-only replicas offloads significant CPU from the primary node. In Report Builder's connection settings, use the standard XMLA connection string without ReadWrite specified, and the query router will direct queries to replicas automatically.
A scale-out deployment without proper observability is a liability, not an asset. You need to know how many replicas are active, how queries are being distributed, and whether individual replicas are falling behind in synchronization.
Power BI Premium supports native integration with Azure Log Analytics for query-level telemetry. Enable this in the Admin Portal under Azure connections > Log Analytics workspace. Once connected, the Power BI service streams detailed query logs — including which node served each query — to your Log Analytics workspace.
Navigate to your Azure Portal, open your Log Analytics workspace, and query the PowerBIDatasetsWorkspace table. Here's a KQL query that gives you per-replica query distribution over a time window:
PowerBIDatasetsWorkspace
| where TimeGenerated > ago(24h)
| where OperationName == "QueryEnd"
| extend ReplicaId = tostring(parse_json(ExtendedProperties).ReplicaId)
| extend DurationMs = tolong(parse_json(ExtendedProperties).QueryDuration)
| summarize
QueryCount = count(),
AvgDurationMs = avg(DurationMs),
P95DurationMs = percentile(DurationMs, 95),
P99DurationMs = percentile(DurationMs, 99)
by ReplicaId, bin(TimeGenerated, 1h)
| order by TimeGenerated desc, QueryCount desc
This query tells you how many queries each replica is handling and whether query latency differs significantly between replicas. If one replica has consistently higher P95 latency, it may be under-resourced or experiencing synchronization issues.
Build a monitoring query that tracks how long synchronization takes after each refresh:
PowerBIDatasetsWorkspace
| where TimeGenerated > ago(7d)
| where OperationName in ("SyncStart", "SyncEnd")
| extend DatasetId = tostring(parse_json(ExtendedProperties).DatasetId)
| summarize
SyncStart = minif(TimeGenerated, OperationName == "SyncStart"),
SyncEnd = maxif(TimeGenerated, OperationName == "SyncEnd")
by DatasetId, bin(TimeGenerated, 1h)
| extend SyncDurationSeconds = datetime_diff('second', SyncEnd, SyncStart)
| where isnotnull(SyncDurationSeconds)
| project TimeGenerated, DatasetId, SyncDurationSeconds
| order by TimeGenerated desc
Plot this as a time series in Azure Monitor or a Power BI report and set an alert when synchronization duration exceeds your SLA threshold (typically 2x the average sync duration for a given dataset).
Create a Power BI report that consolidates replica health using the REST API as a data source. Use Power BI's Web connector to call the sync status endpoint periodically, or use a Power Automate flow that polls the API on a schedule and writes results to a SharePoint list or Azure SQL table that your monitoring report reads.
A practical monitoring dashboard should display:
Incremental refresh reduces the volume of data processed during each refresh cycle by only updating partitions that contain new or changed data. Combined with scale-out, this creates an extremely efficient architecture for large datasets:
Enable incremental refresh in Power BI Desktop using the RangeStart and RangeEnd parameters and configure it to store 36 months historically while refreshing only the last 3 days. With scale-out enabled, your replica sync times drop dramatically because only the changed partitions need to propagate — not the full dataset.
Tip: When using incremental refresh with scale-out, set your incremental refresh policy to only refresh complete days (not the current partial day) unless you're specifically building near-real-time dashboards. This prevents stale partial-day data from creating confusing discrepancies across replicas.
If you're using Power BI Deployment Pipelines to manage development, test, and production environments, scale-out configuration must be part of your deployment process. Dataset settings — including scale-out — are workspace-level properties, not embedded in the .pbix file, so they don't automatically propagate through pipeline stages.
Build a post-deployment script that configures scale-out settings after each pipeline deployment:
function Set-PostDeploymentDatasetConfig {
param(
[string]$WorkspaceId,
[hashtable]$DatasetScaleOutConfig
)
foreach ($dataset in $DatasetScaleOutConfig.Keys) {
$config = $DatasetScaleOutConfig[$dataset]
# Find the dataset ID by name in the target workspace
$datasets = Invoke-PowerBIRestMethod `
-Url "https://api.powerbi.com/v1.0/myorg/groups/$WorkspaceId/datasets" `
-Method Get | ConvertFrom-Json
$targetDataset = $datasets.value | Where-Object { $_.name -eq $dataset }
if ($null -eq $targetDataset) {
Write-Warning "Dataset '$dataset' not found in workspace $WorkspaceId"
continue
}
$body = @{
queryScaleOutSettings = @{
autoSyncReadOnlyReplicas = $config.AutoSync
maxReadOnlyReplicas = $config.MaxReplicas
}
} | ConvertTo-Json
Invoke-PowerBIRestMethod `
-Url "https://api.powerbi.com/v1.0/myorg/groups/$WorkspaceId/datasets/$($targetDataset.id)" `
-Method Patch `
-Body $body `
-ContentType "application/json"
Write-Host "Configured scale-out for '$dataset': $($config.MaxReplicas) replicas, AutoSync=$($config.AutoSync)"
}
}
# Configuration map for your production workspace
$productionScaleOutConfig = @{
"Enterprise Finance Model" = @{ MaxReplicas = 3; AutoSync = $false }
"Sales Analytics Platform" = @{ MaxReplicas = 2; AutoSync = $true }
"Supply Chain Operations" = @{ MaxReplicas = 2; AutoSync = $true }
}
Set-PostDeploymentDatasetConfig `
-WorkspaceId "prod-workspace-guid-here" `
-DatasetScaleOutConfig $productionScaleOutConfig
Run this script as part of your CI/CD pipeline immediately after a deployment pipeline promotion to production.
Many enterprises use third-party tools — Tableau, Qlik Sense, custom applications with MDX/DAX queries — that connect to Power BI Premium datasets via the XMLA endpoint. These tools generally don't understand the Power BI-specific connection routing parameters. The default behavior is that XMLA connections without explicit DataSourceType specification are treated as read-only and routed to replicas.
This is usually what you want, but verify it explicitly. Some connection libraries may send session properties that inadvertently signal a read-write intent, causing the query router to direct them to the primary node. If your third-party tool connections are consistently hitting the primary even with scale-out enabled, check the XMLA session properties being sent and consult the tool's documentation for connection string customization.
This exercise walks you through a complete scale-out implementation for a realistic enterprise scenario: the Northwind Enterprise Analytics semantic model, a 12 GB dataset serving finance, operations, and executive teams across a P2 Premium capacity.
You'll need:
Connect to your dataset via the XMLA endpoint in SSMS. Use the connection string format powerbi://api.powerbi.com/v1.0/myorg/<YourWorkspaceName> and authenticate with your organizational account.
Run the following to get the total in-memory size:
SELECT
SUM(USED_SIZE) / 1073741824.0 as TotalMemoryGB,
SUM(DICTIONARY_SIZE) / 1073741824.0 as DictionaryGB,
COUNT(DISTINCT DIMENSION_NAME) as TableCount
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNS
Record the TotalMemoryGB value. For the exercise, assume this returns 11.4 GB. With three replicas, you need 11.4 × 4 = 45.6 GB of dataset memory capacity available. A P2 provides 50 GB — workable, but tight if other datasets share the capacity.
Generate a Power BI service principal or use your user account's Bearer token. Using PowerShell:
Connect-PowerBIServiceAccount
# Get your workspace and dataset IDs
$workspace = Get-PowerBIWorkspace -Name "Northwind Analytics Production"
$dataset = Get-PowerBIDataset -WorkspaceId $workspace.Id |
Where-Object { $_.Name -eq "Northwind Enterprise Analytics" }
Write-Host "Workspace ID: $($workspace.Id)"
Write-Host "Dataset ID: $($dataset.Id)"
# Enable scale-out with auto-sync
$body = @{
queryScaleOutSettings = @{
autoSyncReadOnlyReplicas = $true
maxReadOnlyReplicas = 2
}
} | ConvertTo-Json
Invoke-PowerBIRestMethod `
-Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($dataset.Id)" `
-Method Patch `
-Body $body `
-ContentType "application/json"
Write-Host "Scale-out enabled. Waiting for replicas to provision..."
Wait 3-5 minutes after enabling scale-out, then check the sync status:
$statusUrl = "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($dataset.Id)/queryScaleOut/syncStatus"
$status = Invoke-PowerBIRestMethod -Url $statusUrl -Method Get | ConvertFrom-Json
$status | ConvertTo-Json -Depth 5
Verify that replicasSyncState shows Succeeded and the syncEndTime is recent (within the last few minutes).
Open several browser tabs, log in as different users (or use incognito sessions with different accounts), and access your reports simultaneously. In parallel, watch the Log Analytics workspace:
PowerBIDatasetsWorkspace
| where TimeGenerated > ago(30m)
| where OperationName == "QueryEnd"
| extend ReplicaId = tostring(parse_json(ExtendedProperties).ReplicaId)
| summarize QueryCount = count() by ReplicaId
You should see queries distributed across your primary and replica nodes. If all queries show the same ReplicaId, the router isn't distributing load — check that the reports aren't using explicit ReadWrite connections.
Disable auto-sync and practice the controlled synchronization pattern:
# Disable auto-sync
$body = @{
queryScaleOutSettings = @{
autoSyncReadOnlyReplicas = $false
maxReadOnlyReplicas = 2
}
} | ConvertTo-Json
Invoke-PowerBIRestMethod `
-Url "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($dataset.Id)" `
-Method Patch -Body $body -ContentType "application/json"
# Trigger a refresh (or wait for a scheduled one to complete)
# Then manually initiate sync
$syncUrl = "https://api.powerbi.com/v1.0/myorg/groups/$($workspace.Id)/datasets/$($dataset.Id)/queryScaleOut/sync"
Invoke-PowerBIRestMethod -Url $syncUrl -Method Post
# Poll until done
do {
Start-Sleep -Seconds 15
$status = Invoke-PowerBIRestMethod -Url $statusUrl -Method Get | ConvertFrom-Json
Write-Host "State: $($status.replicasSyncState) at $(Get-Date -Format 'HH:mm:ss')"
} while ($status.replicasSyncState -notin @("Succeeded", "Failed"))
Record the total sync duration. This is your baseline for SLA planning.
Symptom: After enabling scale-out, reports become slower or datasets are evicted from memory more frequently. The capacity utilization metrics show memory pressure spikes after refresh operations.
Root cause: The replica copies require the same memory as the primary. Enabling 2 replicas on a dataset that already uses 70% of your capacity's memory causes total dataset memory to exceed capacity, triggering evictions.
Fix: Profile all datasets' in-memory sizes, calculate total replica memory requirements, and right-size the capacity before enabling scale-out. Use DISCOVER_STORAGE_TABLE_COLUMNS DMV queries across all datasets on the capacity to get a complete picture.
Symptom: Users report seeing different data in the same report at the same time, or data that appears "stale" compared to the source system shortly after a refresh.
Root cause: Read-only replicas haven't finished synchronizing when users begin querying. Different users hit different replicas at different stages of synchronization.
Fix: For reports where consistency is critical, either use the manual sync pattern (controlled synchronization after validation) or implement a visual indicator in reports showing the data's Last Refreshed timestamp. You can surface this via a calculated measure:
Last Refreshed Timestamp =
"Data as of " & FORMAT(MAX('RefreshLog'[RefreshCompletedAt]), "YYYY-MM-DD HH:MM AM/PM")
Include this in a report header card so users know exactly which data version they're viewing.
Symptom: XMLA write operations fail with permissions errors. Deployment scripts that push model changes fail intermittently.
Root cause: Deployment scripts or XMLA tools using the default connection string (without DataSourceType=ReadWrite) are routed to read-only replicas, which reject write operations.
Fix: Always specify DataSourceType=ReadWrite in the XMLA connection strings used by deployment and administrative scripts. Create a documented standard for your team: use ReadOnly for read operations in monitoring tools, ReadWrite for administrative and deployment connections.
Symptom: Even with scale-out enabled, reports are still slow during the refresh window. Primary node CPU utilization drops (refresh is happening there), but users are experiencing slow query responses.
Root cause: During replica synchronization, replicas are loading the new dataset image and are temporarily less available for query serving. If the sync window is long relative to your refresh frequency, there's a period where fewer replicas are fully operational.
Fix: Combine scale-out with incremental refresh to shorten both the primary refresh duration and the replica sync window. For a 12 GB model, incremental refresh on the hot partition (last 3 days) might reduce the refreshed data volume to 500 MB, dropping sync time from 3 minutes to 30 seconds.
Symptom: Scale-out works perfectly in production but isn't configured in test environments, causing test performance to differ significantly from production behavior. Bugs related to replica consistency go undetected until they reach production.
Root cause: Deployment pipelines don't carry scale-out settings between stages.
Fix: Include scale-out configuration in your deployment runbook as an explicit post-deployment step, applied to every pipeline stage that should mirror production behavior. Even in test environments, enable at least one replica so replica-specific behaviors can be tested.
If replicasSyncState consistently returns Failed, investigate in this order:
SyncFailed events with extended properties that include error codes.maxReadOnlyReplicas to 1 to rule out resource exhaustion from trying to sync multiple replicas simultaneously.Query Scale-Out is one of the most impactful architectural tools in the Power BI Premium toolkit, but it demands more than a feature toggle. The implementation requires careful capacity sizing based on real in-memory footprints, deliberate decisions about synchronization strategy (auto vs. controlled), clear communication with report consumers about the consistency model, and robust monitoring instrumentation.
The key ideas to carry forward:
DataSourceType parameters for fine-grained control.From here, the natural progression deepens your enterprise Power BI architecture skills:
The organizations that get the most from Query Scale-Out are those that integrate it into a broader capacity governance practice — with documented sizing standards, automated deployment configuration, active monitoring dashboards, and regular capacity reviews. It's not a set-and-forget feature; it's a component of a living, managed enterprise data platform.