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

Ondrej SebelaOndrej Sebela
7 min read

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

  1. Install my PowerShell module MSGraphStuff

    1.   Install-Module MSGraphStuff
      
  2. Create a batch request using New-GraphBatchRequest function

    1.    $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.   $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.

💡
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

  1. Improved Performance

    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 Tips to find out how to find 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.

    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 todo 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 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 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 😎

0
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).