Yeah, I know the pain. You run a straightforward Resource Graph query, it works fine on small scopes, then you point it at a production subscription with thousands of VMs and it just hangs. Then you get a GatewayTimeout or a throttling response. Annoying as hell.
Here's the fix — and it's not what Microsoft docs will tell you first. They'll say 'use paginate', but that's only half the story. The real fix is splitting your query by subscription and then paginating each chunk. Let me show you.
The Direct Fix: Split and Paginate
If you're querying across multiple subscriptions or a massive single one, you've got two problems. First, Resource Graph has a server-side timeout (around 5 seconds if it doesn't return). Second, it throttles per principal — about 15 requests per 5 seconds. So you need to break the query into smaller pieces and pull each piece with pagination.
Here's a PowerShell example that does it right:
$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq 'Enabled' }
$allResults = @()
foreach ($sub in $subscriptions) {
$query = "resources | where type == 'microsoft.compute/virtualmachines'
| project name, resourceGroup, location"
$pageSize = 1000
$skip = 0
do {
$result = Search-AzGraph -Query $query -Subscription $sub.SubscriptionId `
-First $pageSize -Skip $skip -MaxRetryCount 3
$allResults += $result
$skip += $pageSize
} while ($result.Count -eq $pageSize)
}
Note the -MaxRetryCount 3 — that handles transient throttling. Also -First and -Skip are your friends. Without them, you're asking for everything at once.
If you're stuck with the REST API directly
Same principle. Use the $skiptoken parameter to page through results. Here's a quick curl-like snippet:
POST https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01
Authorization: Bearer {token}
Content-Type: application/json
{
"subscriptions": ["sub1-id", "sub2-id"],
"query": "resources | limit 1000",
"options": {
"$skip": 0,
"$top": 1000
}
}
Then check the response headers for x-ms-request-id and if you get a 429, back off and retry with exponential delay — 2 seconds, 4, 8. Don't hammer it.
Why This Actually Works
Resource Graph is a distributed service. When you query a big scope, it has to fan out across multiple nodes, aggregate, and sort — that takes time. Over 5 seconds, the gateway gives up. Splitting by subscription reduces the data each request has to scan. Pagination means you never ask for more than 1000 rows at once, which keeps the response time under the limit.
Also, the throttling limit is per principal per 5 seconds. If you have 10 subscriptions and you do one query per second, you'll hit the 15-request cap. So you need to add a small delay between requests if you're going fast. I usually throw in Start-Sleep -Milliseconds 300 between subscriptions. You won't notice the difference, but you'll stop seeing 429 Too Many Requests.
One more thing: Search-AzGraph has a built-in retry logic, but it's not aggressive enough. That's why I set -MaxRetryCount explicitly. The default is 0, believe it or not. So any transient bottleneck and you're dead.
Less Common Variations of the Same Issue
Sometimes the timeout isn't about query size — it's about a specific operator that's expensive. Here are a few culprits:
- Joins across tables — like joining
resourceswithresourcecontainersorauthorizationresources. That's a cross-node operation, and it'll slow you down fast. Try to avoid it or pre-filter usingwherebefore the join. - Sorting without a filter —
project name, type | order by nameon a huge dataset forces a full scan and sort. Add awhereclause that narrows things down first. - Using
containsinstead ofstartswith—containsdoes a substring search, which is slower. If you can usestartswithor even an equality check, do it. - Querying all subscription IDs inline — if you pass a list of 50 subscriptions in one request, that's a huge scope. Split it into batches of 10 or 20.
Then there's the sneaky case where you're not hitting the timeout but you're getting partial results without realizing it. Resource Graph can return fewer rows than you expect if it hits the max result size (100 MB, I think). You'll see a warning in the response header warning field. Always check for that. If it's truncated, you need to add a filter or split by something like resource group.
Prevention: Build Queries That Scale
Don't wait for the timeout to happen. Build your queries with scaling in mind from the start.
- Always filter by subscription or resource group when you can. Don't query the whole tenant unless you absolutely have to.
- Use
projectto limit columns — less data to transfer and process. - Use
summarizeearly — if you need counts or aggregations, do them as close to the source as possible. - Set
pageSizeto 1000 — that's the sweet spot. Too small means more requests, too large risks timeouts. - Write your queries in KQL and test them on a small scope first. If it's slow on one subscription, it'll be catastrophic on ten.
Also, consider using Azure Resource Graph's first and skip parameters in your app logic rather than trying to pull everything at once. You can build a simple loop that pages through results and writes them to a file or database. That way you're never holding thousands of rows in memory.
And one last pro tip: if you're doing this from a script or a function, set the api-version to the latest stable one — right now that's 2021-03-01. The newer versions have better performance and less aggressive throttling. Don't use the preview ones unless you like surprises.
So there you go. Stop throwing big queries at Resource Graph and expecting it to handle them. Split, paginate, and be nice with retry logic. You'll be back to getting your results in seconds instead of waiting for a timeout.