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

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](https://www.powershellgallery.com/packages/MSGraphStuff) 👍.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">There is a similar <a target="_self" rel="noopener noreferrer nofollow" href="https://doitpshway.com/supercharge-your-azure-api-calls-master-azure-resource-manager-batching-with-powershell" style="pointer-events: none"><strong>batching feature for the Azure Resource Graph Api</strong></a> <a target="_self" rel="noopener noreferrer nofollow" href="https://doitpshway.com/how-to-use-microsoft-graph-api-batching-to-speed-up-your-scripts" style="pointer-events: none">😎</a></div>
</div>

---

## [Performance boost examples](https://doitpshway.com/how-to-use-microsoft-graph-api-batching-to-speed-up-your-scripts)

[In the imag](https://doitpshway.com/how-to-use-microsoft-graph-api-batching-to-speed-up-your-scripts)e 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 😎

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1753269656357/21d35d27-2295-44db-b588-c4c83b229d4c.png align="center")

There is a table of some of my other functions that were rewritten to use batching, along with the performance gain I achieved.

| **Command** | **Speed without batching** | **Speed with batching** | **Source** |
| --- | --- | --- | --- |
| single api call (using Invoke-MgGraphRequest) | <mark>0,11 s</mark> | 0,13 s |  |
| Get-IntuneDeviceHardware | 79 s | <mark>15 s</mark> | [IntuneStuff](https://www.powershellgallery.com/packages/IntuneStuff) |
| Get-IntunePolicy | 50 s | <mark>11 s</mark> | [IntuneStuff](https://www.powershellgallery.com/packages/IntuneStuff) |
| Get-IntuneDiscoveredApp | 145 s | <mark>29 s</mark> | [IntuneStuff](https://www.powershellgallery.com/packages/IntuneStuff) |
| Get-PIMGroup | 88 s | <mark>23 s</mark> | [AzurePIMStuff](https://www.powershellgallery.com/packages/AzurePIMStuff) |
| device-backup-pipeline.yml | 280 s | <mark>65 s</mark> | [https://doitpshway.com/exporting-bitlocker-laps-and-filevault-keys-from-intune-to-git-using-azure-devops-pipeline](https://doitpshway.com/exporting-bitlocker-laps-and-filevault-keys-from-intune-to-git-using-azure-devops-pipeline) |

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">FYI, when comparing <strong>batching </strong>versus PowerShell Core native <strong>parallel </strong>processing (<code>Foreach-Object -Parallel</code>), you get 42s (parallel) vs 29s (batching) in the function <code>Get-IntuneDiscoveredApp</code></div>
</div>

---

# TL;DR

1. Install my PowerShell module [MSGraphStuff](https://www.powershellgallery.com/packages/MSGraphStuff)
    
    1. ```powershell
              Install-Module MSGraphStuff
        ```
        
2. Create a batch request using `New-GraphBatchRequest` function
    
    1. ```powershell
               $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
              )
        ```
        
3. Invoke the batch request using `Invoke-GraphBatchRequest` function to get the results
    
    1. ```powershell
              $batchResult = Invoke-GraphBatchRequest -batchRequest $batchRequest -graphVersion "beta" -Verbose
              
              # output all results at once
              $batchResult
              
              # split the results by the request id, so you can use them separately
              $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](https://learn.microsoft.com/en-us/graph/json-batching?tabs=http), 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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">There are other options for speeding up your code, like PowerShell Core <a target="_self" rel="noopener noreferrer nofollow" href="https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-feature/" style="pointer-events: none">Foreach-Object -Parallel</a> feature. And it definitely has its place, but nothing is as fast as batching.</div>
</div>

## Batching advantages

1. **Huge performance boost**
    
    Parallel processing of up to 20 requests.
    
2. **Reduced Network Overhead**  
    Instead of sending multiple HTTP requests, batching consolidates them into one, reducing the number of round-trips between client and server.
    
3. **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)

1. **You need to know the requested Graph Api URL**
    
    1. **PROBLEM**: You cannot use PowerShell commands like `Get-MgUser`, but the under-the-hood-used URL instead.
        
    2. **SOLUTION**: Check [the Tips](https://doitpshway.com/how-to-use-microsoft-graph-api-batching-to-speed-up-your-scripts#heading-how-to-find-out-the-graph-api-request-url) to find out how to determine the correct API URL.
        
2. **Complex error handling**
    
    1. **PROBLEM**: Each sub-request in a batch can succeed or fail independently, requiring more sophisticated error-handling logic.
        
    2. **SOLUTION**: `Invoke-GraphBatchRequest` handles all server-side errors by retrying the request.
        
3. **Rate limiting still applies**
    
    1. **PROBLEM**: Batching doesn’t bypass Microsoft Graph’s throttling policies. If you exceed limits, your batch requests can still [be throttled](https://learn.microsoft.com/en-us/graph/throttling#throttling-and-batching).
        
    2. **SOLUTION**: `Invoke-GraphBatchRequest` retries the request(s) after the time specified in the server response.
        
4. **Pagination still applies**
    
    1. **PROBLEM**: Each sub-request in a batch can return only one page of the total number of results, requiring special handling logic.
        
    2. **SOLUTION**: `Invoke-GraphBatchRequest` handles pagination by creating another batch of paginated requests (URLs taken from `@odata.nextLink` property).
        
5. **Separate API versions**
    
    1. **PROBLEM**: You can’t combine requests against *beta* and *v1.0* api endpoints in the same batch.
        
    2. **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).
        
6. **20 requests per batch limitation**
    
    1. **PROBLEM**: When sending a batch request to `https://graph.microsoft.com/<apiVersion>/$batch` you cannot send more than 20 requests per batch.
        
    2. **SOLUTION**: `Invoke-GraphBatchRequest` handles batch-requests-limit by automatically splitting batch requests into chunks of 20.
        
7. **Payload size limits**
    
    1. **PROBLEM**: The total size of a batch request is limited (typically 4 MB), which can be restrictive for large data operations.
        
    2. **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](https://www.powershellgallery.com/packages/IntuneStuff/1.6.4) 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](https://www.powershellgallery.com/packages/IntuneStuff/1.6.4)). The same applies to **discovered apps** ([Get-IntuneDiscoveredApp](https://www.powershellgallery.com/packages/IntuneStuff/1.6.4)) 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](https://www.powershellgallery.com/packages/IntuneStuff/1.7.0) 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`
    
    * `GET` in most cases
        
* Request `url`
    
    * in relative form (without the 'https://graph.microsoft.com/&lt;apiversion&gt;' prefix)
        

You can create it manually like below

```powershell
# 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

```powershell
$batchRequest = @((New-GraphBatchRequest -Url "applications" -Id "app"), (New-GraphBatchRequest -Url "servicePrincipals" -Id "sp"))
```

And then use it like

```powershell
# 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](https://learn.microsoft.com/en-us/graph/json-batching?tabs=http#creating-a-batch-request) and [examples](https://learn.microsoft.com/en-us/graph/json-batching?tabs=http#example-json-batch-request) 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.

```powershell
$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)
    
* ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1750005082967/3418540b-0f2e-44ed-bd3a-77bbff793fd9.png align="center")
    
    You can find any `-Mg*` cmdlet using `Find-MgGraphCommand` to get the called (relative) base URL
    
* ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749236399588/d18b9d01-3eef-4387-9520-be4c369add66.png align="center")
    

Or if you like, you can search the [official documentation](https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0) :)

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749237140193/75ebd2a9-dc49-46b0-995c-8d788a651d3e.png align="center")

As you can see Azure portal uses batching for Groups retrieval :)

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Beware that in case of batch requests, you have to check <code>Payload</code> sub-tab to get the actual request URL.</div>
</div>

When batching is NOT used, you can get the requested URL in the `Headers` sub-tab

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749237748075/0c2caaf4-fbf7-48a7-b42e-b7b9f74e3fa2.png align="center")

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">There is also <a target="_self" rel="noopener noreferrer nofollow" href="https://chromewebstore.google.com/detail/gdhbldfajbedclijgcmmmobdbnjhnpdh?utm_source=item-share-cb" style="pointer-events: none">Chrome extension X-Ray</a> that filters those Graph Api urls for you, but I am personally a little bit afraid of using any extension under my administrator account :)</div>
</div>

---

# 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](https://www.powershellgallery.com/packages/MSGraphStuff) module) to overcome the lack of built-in support for pagination, throttling, and server-side errors handling.

Happy coding 😎
