Graph Api batching is a great way to improve the performance of your Graph API-related scripts dramatically.
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 👍.
In the image below, you can see the huge performance boost provided by using batching. From 50 seconds, I was able to pull all my Intune policies in just 11 seconds. And the difference can be even bigger in larger environments 😎

There is a table of some of my other functions that were rewritten to use batching, along with the performance gain I achieved.
💡
FYI, when comparing batching versus PowerShell Core native parallel processing (Foreach-Object -Parallel), you get 42s (parallel) vs 29s (batching) in the function Get-IntuneDiscoveredApp
TL;DR
Install my PowerShell module MSGraphStuff
Install-Module MSGraphStuff
Create a batch request using New-GraphBatchRequest function
$batchRequest = @(
New-GraphBatchRequest -url "/applications" -id "Apps"
New-GraphBatchRequest -url "/servicePrincipals" -id "SPs"
New-GraphBatchRequest -url "/users" -id "Users"
New-GraphBatchRequest -url "/groups" -id "Groups"
New-GraphBatchRequest -url "/deviceManagement/managedDevices" -id "IntuneDevices"
)
Invoke the batch request using Invoke-GraphBatchRequest function to get the results
$batchResult = Invoke-GraphBatchRequest -batchRequest $batchRequest -graphVersion "beta" -Verbose
$batchResult
$apps = $batchResult | ? RequestId -eq "Apps"
$sps = $batchResult | ? RequestId -eq "SPs"
$users = $batchResult | ? RequestId -eq "Users"
$groups = $batchResult | ? RequestId -eq "Groups"
$intuneDevices = $batchResult | ? RequestId -eq "IntuneDevices"
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.
💡
There are other options for speeding up your code, like PowerShell Core
Foreach-Object -Parallel feature. And it definitely has its place, but nothing is as fast as batching.
Batching advantages
Huge performance boost
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 the Tips to find out how to determine 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 to-do 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
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!
Automatic Api throttling on specific URIs when ‘expand’ is used
For example, when working with /deviceManagement/configurationPolicies?$expand=settings,assignments you will encounter automatic throttling on the server side. To overcome this problem, you have to split the calls: /deviceManagement/configurationPolicies/<policyId>/settings, /deviceManagement/configurationPolicies/<policyId>/assignments. Btw, this is what I am doing in my improved Get-IntunePolicy function, where thanks to batching, I was able to reduce the run time from 60 seconds to just 8 (in my environment).
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
Request url
- in relative form (without the 'https://graph.microsoft.com/<apiversion>' prefix)
You can create it manually like below
$batchRequest = @(
[PSCustomObject]@{
id = "app"
method = "GET"
URL = "applications"
},
[PSCustomObject]@{
id = "sp"
method = "GET"
URL = "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
$allResults = Invoke-GraphBatchRequest -batchRequest $batchRequest
$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 used filter and property parameters)

You can find any -Mg* cmdlet using Find-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 :)
💡
Beware that in case of batch requests, you have to check 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

💡
There is also
Chrome extension X-Ray that filters those Graph Api urls for you, but I am personally a little bit afraid of using any extension under my administrator account :)
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 😎