How to use Microsoft Graph Api Batching to speed up your scripts

Table of contents

Graph Api batching is a great way to dramatically improve the performance of your Graph API-related scripts.
It enables parallel execution of up to 20 Graph API calls, which is fantastic, but there is one tiny little problem. You have to write your own logic for managing pagination, throttling, server-side errors recovery, and more.
Well, you don’t have to anymore, because of my new functions New-GraphBatchRequest
, Invoke-GraphBatchRequest
hosted in the PowerShell module MSGraphStuff 👍.
TL;DR
Install my PowerShell module MSGraphStuff
Install-Module MSGraphStuff
Create a batch request using
New-GraphBatchRequest
function$batchRequest = @( # Azure app registrations New-GraphBatchRequest -url "/applications" -id "Apps" # Azure enterprise applications New-GraphBatchRequest -url "/servicePrincipals" -id "SPs" # Azure users New-GraphBatchRequest -url "/users" -id "Users" # Azure groups New-GraphBatchRequest -url "/groups" -id "Groups" # Intune devices New-GraphBatchRequest -url "/deviceManagement/managedDevices" -id "IntuneDevices" # ... you can add as many Api urls as you wish )
Invoke the batch request using
Invoke-GraphBatchRequest
function to get the results$batchResult = Invoke-GraphBatchRequest -batchRequest $batchRequest -graphVersion "beta" -Verbose # output all results $batchResult # split the results by the request id $apps = [System.Collections.Generic.List[Object]]::new() $sps = [System.Collections.Generic.List[Object]]::new() $users = [System.Collections.Generic.List[Object]]::new() $groups = [System.Collections.Generic.List[Object]]::new() $intuneDevices = [System.Collections.Generic.List[Object]]::new() $batchResult | % { $result = $_ switch ($result.RequestId) { "Apps" { $apps.Add($result ) } "SPs" { $sps.Add($result ) } "Users" { $users.Add($result ) } "Groups" { $groups.Add($result ) } "IntuneDevices" { $apps.Add($result ) } Default { throw "Undefined type" } } }
Graph Api Batching introduction
What is Graph Api batching?
According to the official documentation, Graph Api JSON batching allows clients to combine multiple requests into a single JSON object and a single HTTP call, reducing network roundtrips and improving efficiency. Microsoft Graph supports batching up to 20 requests into the JSON object.
To put it simply, batching allows you to process up to 20 Graph Api requests at the same time 😎
Most of the Microsoft portals use batching under the hood, btw.
Batching advantages
Improved Performance
Parallel processing of up to 20 requests.
Reduced Network Overhead
Instead of sending multiple HTTP requests, batching consolidates them into one, reducing the number of round-trips between client and server.Bypassing URL length limitations
In cases where the filter clause is complex, the URL length might surpass limitations built into browsers or other HTTP clients. You can use JSON batching as a workaround for running these requests because the lengthy URL simply becomes part of the request payload.
Batching drawbacks (and how I solved them)
You need to know the requested Graph Api URL
PROBLEM: You cannot use PowerShell commands like
Get-MgUser
, but the under-the-hood-used URL instead.SOLUTION: Check Tips to find out how to find the correct Api URL.
Complex error handling
PROBLEM: Each sub-request in a batch can succeed or fail independently, requiring more sophisticated error-handling logic.
SOLUTION:
Invoke-GraphBatchRequest
handles all server-side errors by retrying the request.
Rate limiting still applies
PROBLEM: Batching doesn’t bypass Microsoft Graph’s throttling policies. If you exceed limits, your batch requests can still be throttled.
SOLUTION:
Invoke-GraphBatchRequest
retries the request(s) after the time specified in the server response.
Pagination still applies
PROBLEM: Each sub-request in a batch can return only one page of the total number of results, requiring special handling logic.
SOLUTION:
Invoke-GraphBatchRequest
handles pagination by creating another batch of paginated requests (URLs taken from@odata.nextLink
property).
Separate API versions
PROBLEM: You can’t combine requests against beta and v1.0 api endpoints in the same batch.
SOLUTION: Currently, none. But it’s in my todo to allow requesting both api versions in the
Invoke-GraphBatchRequest
(by separating the batches by inner logic).
20 requests per batch limitation
PROBLEM: When sending a batch request to
https://graph.microsoft.com/<apiVersion>/$batch
you cannot send more than 20 requests per batch.SOLUTION:
Invoke-GraphBatchRequest
handles batch-requests-limit by automatically splitting batch requests into chunks of 20.
Payload size limits
PROBLEM: The total size of a batch request is limited (typically 4 MB), which can be restrictive for large data operations.
SOLUTION: None. In the size of my company, I haven’t encountered this limitation.
Frankly, those drawbacks kept me away from using batching for quite a long time.
One of the things that finally made me adopt the batching and solve the mentioned issues was working on my Get-IntuneDeviceHardware function. A Graph Api URL that needs to be used to get hardware information requires querying devices one by one, which can be super slow!
Use cases
Information that can be gathered only one-at-a-time
As mentioned, Intune device hardware inventory data must be requested device by device (Get-IntuneDeviceHardware). The same applies to discovered apps (Get-IntuneDiscoveredApp) or getting Bitlocker keys, FileVault keys, or a lot of PIM-related stuff.
You are making more than one Graph Api request in your code
It doesn’t make sense to use batching for one request. But more than one? Worth it!
Tips
How to create a batch request for the Invoke-GraphBatchRequest function
Invoke-GraphBatchRequest
function accepts an array of requests PSObjects (via batchRequest
parameter) where at minimum, you have to specify the following properties:
Request
id
- can be later used to separate the results
HTTP
method
GET
in most cases
Request
url
- in relative form (without the 'https://graph.microsoft.com/<apiversion>' prefix)
You can create it manually like below
# create batch request
$batchRequest = @(
[PSCustomObject]@{
id = "app"
method = "GET"
URL = "applications" # stands for https://graph.microsoft.com/<apiversion>/applications
},
[PSCustomObject]@{
id = "sp"
method = "GET"
URL = "servicePrincipals" # stands for https://graph.microsoft.com/<apiversion>/servicePrincipals
}
)
Or via my function New-GraphBatchRequest
like this
$batchRequest = @((New-GraphBatchRequest -Url "applications" -Id "app"), (New-GraphBatchRequest -Url "servicePrincipals" -Id "sp"))
And then use it like
# run batch request
$allResults = Invoke-GraphBatchRequest -batchRequest $batchRequest
# separate the results by request id
$applicationList = $allResults | ? RequestId -eq "app"
$servicePrincipalList = $allResults | ? RequestId -eq "sp"
See the documentation and examples for more details.
How to create dozens of requests where only the ID part of the URL is changing
This is exactly why I’ve created New-GraphBatchRequest
function originally, because you can give it a URL with <placeholder>
string inside (url
parameter), plus an array of strings (placeholder
parameter) to generate a customized URL request for each of them.
$deviceId = (Get-MgBetaDeviceManagementManagedDevice -Property id -All).Id
New-GraphBatchRequest -url "/deviceManagement/managedDevices/<placeholder>?`$select=id,devicename&`$expand=DetectedApps" -placeholder $deviceId
How to find out the Graph Api request URL
OK, so you have some function or script that uses official Graph Api SDK cmdlets a.k.a. Get-MgUser
, Get-MgDevice
, … and you want to know what URLs are used under the hood?
You have the following options:
You can add
-Debug
switch to any-Mg*
cmdlet and it will return the called URL (including the usedfilter
andproperty
parameters)You can find any
-Mg*
cmdlet usingFind-MgGraphCommand
to get the called (relative) base URL
Or if you like, you can search the official documentation :)
Or maybe you want to know how the data that you can see on some Intune/Azure/… portal is gathered? In such case, use the developer tools feature (F12) in your browser and on the tab Network
search for graph
string.
As you can see Azure portal uses batching for Groups retrieval :)
Payload
sub-tab to get the actual request URL.When batching is NOT used, you can get the requested URL in the Headers
sub-tab
Summary
Graph Api Batching is a great way to improve the performance of your scripts, but you have to use functions like mine Invoke-GraphBatchRequest
(MSGraphStuff module) to overcome the lack of built-in support for pagination, throttling, and server-side errors handling.
Happy coding 😎
Subscribe to my newsletter
Read articles from Ondrej Sebela directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ondrej Sebela
Ondrej Sebela
I work as System Administrator for more than 10 years now and I love to make my life easier by automating work & personal stuff via PowerShell (even silly things like food recipes list generation).