26 Jun 2026 · Data Engineering, SQL
Microsoft Fabric Just Changed How SQL Endpoint Refresh Works — Here's What You Need to Know
A real-world debugging story in Microsoft Fabric — and what Microsoft just shipped to solve it natively.
A real-world debugging story in Microsoft Fabric — and what Microsoft just shipped to solve it natively.
This morning I ran into a frustrating issue that I suspect many Fabric users have hit without fully understanding why. Our Power BI semantic model refreshed successfully — green tick, no errors — but the data was from yesterday. The pipeline had run, the files were there, everything looked fine. So what went wrong?
Let me walk you through the diagnosis, the solution we built today, and — importantly — what Microsoft has just released that changes the game going forward.
The setup
We’re running a Microsoft Fabric lakehouse architecture:
- Spark notebooks write Delta tables to a Gold lakehouse
- A SQL Analytics Endpoint sits on top of the lakehouse
- A Power BI semantic model reads from that endpoint in Import mode
- Everything is orchestrated via a Fabric pipeline
The pipeline runs daily, writes data, refreshes the SQL endpoint, then triggers the semantic model refresh. Simple enough.
What happened this morning
Looking at the lakehouse file view, the parquet files for ft_sales had landed at 4:18 AM. The SQL endpoint refreshMetadata API was called at 4:20 AM — just 2 minutes later.
But here’s the smoking gun: the metadata folder inside ft_sales showed a last-modified time of 7:40 AM — nearly 3 hours after the files landed.
The SQL endpoint refresh ran before the Delta table transaction log had fully committed. The API saw no changes, returned "NotRun" for ft_sales, and the semantic model refreshed against stale data.
Understanding the root cause
The Microsoft Fabric SQL Analytics Endpoint sits on top of the Delta Lake transaction log, not the raw parquet files. When Spark writes a Delta table, the process is:
- Parquet files written to storage
- Delta transaction log (
_delta_log) updated ← this can lag - Metadata folder updated ← confirmation of commit
- SQL Endpoint detects change via Delta log
- Semantic model sees new data
Calling refreshMetadata between steps 1 and 2 means the endpoint sees nothing new. It’s not a bug — it’s a race condition caused by firing the refresh too eagerly.
There’s also a second problem: there was no mechanism to confirm the SQL endpoint had actually finished syncing before triggering the semantic model refresh. The API returns 202 Accepted asynchronously, but our code was moving on immediately.
Solution 1 — the robust notebook approach (production-ready today)
After debugging this end-to-end, here’s the correct sequence:
Spark writes Delta tables
↓
1. Poll _delta_log until commit files (.json) appear
↓
2. POST /sqlEndpoints/{id}/refreshMetadata (with timeout body)
↓
3. Read per-table sync status from response
↓
4. Safe to trigger semantic model refresh
Here’s the complete notebook, designed to work both standalone and called from a pipeline with parameters.
Resolve which tables to check:
import time
from datetime import datetime, timezone
lakehouse_name = f"{workspace_gold}_LH"
base_path = f"abfss://{workspace_gold}@onelake.dfs.fabric.microsoft.com/{lakehouse_name}.Lakehouse/Tables"
print(f"Base path: {base_path}")
if tables_written_param and tables_written_param.strip():
# Specific tables passed from pipeline
tables_to_check = [t.strip() for t in tables_written_param.split(",")]
print(f"Checking specific tables: {tables_to_check}")
else:
# Scan all Delta tables in the lakehouse
print("No tables specified — scanning all Delta tables...")
tables_to_check = []
try:
all_items = mssparkutils.fs.ls(base_path)
for item in all_items:
if item.isDir:
table_name = item.name.rstrip("/")
delta_log = f"{base_path}/{table_name}/_delta_log"
try:
mssparkutils.fs.ls(delta_log)
tables_to_check.append(table_name)
except:
pass
except Exception as e:
print(f"Could not scan lakehouse path: {e}")
print(f"Found {len(tables_to_check)} Delta tables")
Wait until Delta logs are committed — this is the critical step that was missing before:
MAX_WAIT = 300 # 5 minutes
INTERVAL = 15 # poll every 15 seconds
print("Waiting for Delta commits to settle...\n")
for table_name in tables_to_check:
delta_log = f"{base_path}/{table_name}/_delta_log"
confirmed = False
elapsed = 0
while elapsed < MAX_WAIT:
try:
log_files = [
f for f in mssparkutils.fs.ls(delta_log)
if f.name.endswith(".json")
]
if log_files:
latest_ts = max(f.modifyTime for f in log_files)
last_commit = datetime.fromtimestamp(latest_ts / 1000, tz=timezone.utc)
print(f"{table_name} — last commit: {last_commit}")
confirmed = True
break
else:
print(f"[{elapsed}s] {table_name} — no commits yet, retrying...")
except Exception:
print(f"[{elapsed}s] {table_name} — not ready, retrying...")
time.sleep(INTERVAL)
elapsed += INTERVAL
if not confirmed:
raise TimeoutError(f"{table_name} Delta log never settled after {MAX_WAIT}s")
print("\nAll Delta tables committed.")
Refresh SQL Endpoint and read results:
import requests
import json
token = mssparkutils.credentials.getToken("https://api.fabric.microsoft.com")
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
sql_url = (
f"https://api.fabric.microsoft.com/v1/workspaces/"
f"{workspace_id}/sqlEndpoints/{sql_endpoint_id}/refreshMetadata"
)
# Pass a timeout — the API waits synchronously up to this limit
json_body = {
"timeout": {
"timeUnit": "Minutes",
"value": 5
}
}
print("Refreshing SQL Endpoint...")
response = requests.post(sql_url, headers=headers, json=json_body)
result = response.json()
print(f"Status code: {response.status_code}\n")
# Per-table results
tables = result.get("tables", [])
if tables:
for t in tables:
name = t.get("tableName", "unknown")
status = t.get("status", "unknown")
error = t.get("error", {}).get("message", "") if status == "Failure" else ""
print(f"{name}: {status} {f'— {error}' if error else ''}")
else:
print("No table sync info returned.")
print("\nSQL Endpoint refresh complete — safe to trigger semantic model refresh")
Solution 2 — what Microsoft just shipped (the native path)
While we were debugging this, Microsoft has been working on the exact same problem. Two announcements are directly relevant:
New metadata sync — preview since May 2026
This is the most impactful change. Microsoft released a new metadata sync architecture for the SQL Analytics Endpoint that keeps data available for querying within seconds of it landing in the lakehouse — using a decoupled architecture that detects schema and data changes separately, and triggers an on-demand refresh automatically when an incoming query detects stale data.
In other words: the 3-hour lag we experienced this morning would essentially disappear.
How to enable it:
- Go to your Fabric workspace
- Open Workspace settings
- Navigate to Warehouse settings
- Enable New metadata sync (preview)
Important: this only applies to new SQL analytics endpoints created after enabling the setting. Your existing endpoints are not automatically migrated.
Once enabled, you also get a new T-SQL stored procedure to refresh a specific table on demand, without touching the full endpoint:
EXEC sys.sp_dw_refresh_ext_table 'dbo.ft_sales';
And a DMV to check the actual sync status per table:
SELECT * FROM sys.dm_db_external_tables_log_status;
-- Returns: last_update_time_utc, latest_log_version, is_blocked
Native “Refresh SQL Endpoint” pipeline activity — June 2026
Also just released: a first-class Refresh SQL Endpoint activity in Fabric pipelines, part of the new Lakehouse Utility Suite. It replaces the notebook-based API call entirely — no more requests.post, no more manual polling.
How to set it up in your pipeline:
- Open your Fabric pipeline
- In the Activities pane, search for Refresh SQL Endpoint
- Drag it onto the canvas after your Spark notebook activity
- In Settings, select your workspace and lakehouse
- Connect it with a success dependency from your notebook
What makes it smarter than the raw API call: Fabric automatically determines whether a full refresh, incremental refresh, or no refresh is needed based on actual changes in the underlying data — reducing unnecessary compute.
Your pipeline then looks like this:
Spark notebook (writes Delta tables)
↓ [on success]
Refresh SQL Endpoint activity ← native, no code needed
↓ [on success]
Semantic model refresh
Which solution should you use?
| Notebook approach | New metadata sync | Native pipeline activity | |
|---|---|---|---|
| Available today | Yes | Preview | Yes |
| Requires code | Yes | No | No |
| Per-table visibility | Full | Via DMV | Partial |
| Handles lag automatically | Via polling | Yes, seconds | Yes |
| Works on existing endpoints | Yes | New only | Yes |
| Production-ready | Yes | Preview | Yes |
My recommendation:
- Right now: use the notebook approach from Solution 1 — it’s battle-tested and gives you full visibility.
- Short term: replace the
refreshMetadataAPI call in the notebook with the native pipeline activity. - When new metadata sync goes GA: enable it on new lakehouses and simplify the whole chain significantly.
Key lessons
- The
refreshMetadataAPI is not magic. It detects changes based on the Delta transaction log, not file timestamps. If the log hasn’t committed when you call it, it returns"NotRun"silently. - Polling
_delta_logis the right signal. Checking for.jsoncommit files viamssparkutils.fs.ls()confirms a Delta write is fully committed before proceeding. - The timeout body matters. Without it,
refreshMetadatamay return before the sync completes. Pass{"timeout": {"timeUnit": "Minutes", "value": 5}}to make it wait synchronously. - Read the per-table response. The API returns
"Success","NotRun", or"Failure"per table. This is your audit trail. - Make it parametric. Pass specific table names from the pipeline when you know what changed, or let the notebook auto-scan the full lakehouse.
- Microsoft is closing this gap natively. The new metadata sync and native pipeline activity are the long-term answer. Keep an eye on GA dates.
If you’re running Fabric pipelines with Import mode semantic models and have ever had a “successful” refresh that showed yesterday’s data — this is probably why. And now you have both the fix for today and the roadmap for tomorrow.
Enjoyed this?
Get new articles like this one straight to your inbox.