Improve Microsoft Graph PowerShell Performance with Batching

Batching (also known as JSON batching), can be used with Microsoft Graph PowerShell to improve the performance of existing scripts that need to interact with services in Microsoft 365, Microsoft Entra or Microsoft Intune.

The concept of batching is quite simple. You gather the information to make multiple requests and then submit them as a ‘batch’ to the Microsoft Graph API. This allows you to make many requests at the same time, therefore improving the total time of your scripts. 

What is request batching?

Request batching enables applications to combine multiple requests to Microsoft Graph to improve performance and reduce latency. It can also be utilised in Microsoft Graph PowerShell to help administrators improve the performance of management tasks and scripts.

A request batch is a collection of requests inside a single JSON object, which in PowerShell, can be stored within a variable. The JSON object can be generated in multiple ways, including:

  • By defining it manually between here-strings. 
  • By defining a hashtable and converting it to JSON.
  • By building an object through code and converting it to JSON (most efficient)

The batch can then be submitted to Microsoft Graph using the Invoke-RestMethod or Invoke-MgGraphRequest cmdlets with the POST method.

When should you use batch requests?

Using the request batching mechanism in Microsoft Graph adds complexity to your scripts. So, while it can improve the performance of your code and the ‘time to output’, it generally makes your code less readable to others who may need to read it or inherit it. Therefore, I only recommend batching requests when you must make many requests to Microsoft Graph (over 20).

An example of where it may benefit you is if you are deleting many users or devices from your environment. While it is easy to use filtering with Microsoft Graph to retrieve a list of objects, deleting them would require individual delete requests. Suppose you need to delete 100 objects from your directory, instead of sending 100 individual requests to Microsoft Graph and waiting for each to execute before the next begins. In that case, they can be batched in groups of 20, so you would only need to make 5 requests to Microsoft Graph.

The performance benefits of batching in PowerShell

I have done some practical testing of using batching and the performance benefits. For this, I have created 2000 test users in Microsoft Entra (these users are not licensed or provisioned). 1000 users I have stored in the $1to1000 variable and the other 1000 are stored in the $1001to2000 variable.

This is the first script below that was run to delete 1000 users. This script sent an individual request to delete each user.

Foreach ($user in $1to1000){
        Remove-MgUser -UserId $user.id
}

Using the Measure-Command cmdlet, the total time to completion for deleting the users took 33 seconds

Without batching
Without batching

I then ran the following script, which batches the requests up in groups of 20 and then submits them as a single request.

for($i=0;$i -lt $1001to2000.count;$i+=20){
    $batch = @{}
    $batch['requests'] = ($1001to2000[$i..($i+19)] | select @{n='id';e={$_.id}},@{n='method';e={'DELETE'}},`
    @{n='url';e={"/users/$($_.id)"}})
    Invoke-mggraphrequest -Method POST -URI "https://graph.microsoft.com/v1.0/`$batch" -body ($batch | convertto-json) -OutputType PSObject
}

You can see the total time to completion is 12 seconds, which is a significant improvement.

With batching
With batching

Example batching request

Below is an example JSON batch request for Microsoft Graph. You can see that the body of the request is surrounded by here-strings (@’ ‘@). 

Here-strings can be specified with a single quote (like you see below), or using double quotes instead. The difference is that the single quote takes the text literally and will not expand variables. However, a double quote will allow for expandable variables.

Then, the Invoke-MgGraphRequest cmdlet is used with the POST method. I have also used a backtick ( ` ) in the URI web path. This ensures the $batch string is taken literally and not expanded as a variable, which is required for a batch request.

The $batch variable containing the request body is then used with the -body variable to submit the request.

$batch = @'
{
  "requests": [
    {
      "id": "1",
      "method": "GET",
      "url": "/users/[email protected]"
    },
    {
      "id": "2",
      "method": "GET",
      "url": "/users/[email protected]"
    },
    {
      "id": "3",
      "method": "GET",
      "url": "/users/[email protected]"
    }
  ]
}
'@

$response = Invoke-MgGraphRequest `
-Method POST `
-URI "https://graph.microsoft.com/beta/`$batch" `
-body $batch `
-OutputType PSObject

Saving the response to a variable ensures you can refer back to the data using dot notation. You can use the example below to extract the user names and email addresses for the users defined in the batch request.

$response.responses.body | Select UserPrincipalName, proxyaddresses | ft

Generating batch requests

In my earlier example, I demonstrated the performance benefits of using batch requests. With this, I used a PowerShell For loop to generate the JSON batch for each request, where importantly, each request was a repeat against a different object of the same type. This enables you to efficiently use batching with PowerShell, otherwise it would be impractical. Here is the template code:

for($i=0;$i -lt $objects.count;$i+=20){
    $batch = @{}
    $batch['requests'] = ($objects[$i..($i+19)] | select @{n='id';e={$_.id}},@{n='method';e={'DELETE'}},`
	@{n='url';e={"/users/$($_.id)"}})
    invoke-mggraphrequest -Method POST -URI "https://graph.microsoft.com/v1.0/`$batch" -body ($batch | convertto-json) -OutputType PSObject
}

To break down the logic behind this, a For loop (or the For statement) has three key pieces of information within its syntax, which I have highlighted in the image below on the first line.

For loop understanding

$i = 0. This command will run before the loop begins. It is used to create and initialise a variable with a starting value 0. 

$i -lt $obects.count. This statement is the condition (or rule), and it says to keep performing this loop while the initial variable $i is less than the total amount of objects in the collection (or array).

$i+=20. This is the return statement. This will be executed on each loop, and 20 will be added to the value of $i. So on the first loop $i = 0, then on the next loop $i = 20, then 40 and so on.

This allows the code to loop while adding 20 to $i on each loop, which is essential in the next step. 

The $objects array is highlighted below, and then in square brackets is a range selector. On the first pass of the loop $i = 0, and $i+19 = 19, this will select the first 20 objects within the array to be included in the first batch request.

For loop understanding2

The return statement will then add 20 to $i ready for the second loop; during the second loop, $i = 20 and $i+19 = 39, selecting the next 20 items from the array and adding them to the next batch request. This will do on until $i is less than the total objects in the array. The outcome is that all objects in the array with be submitted to Microsoft Graph within a batch request.

Wrapping up

While batch requests can drastically improve the performance of your scripts, use them wisely, as the added complexity could be an issue when working collaboratively on scripts, especially if the overall performance impact is minimal. 

There are also many different ways to you can generate batch requests. I demonstrated above how to generate requests programmatically using the For statement in PowerShell. This works for repeating request types. However, an alternative method maybe to store request information outside of your script (such as in a CSV file), then importing it through code.

Daniel Bradley

My name is Daniel Bradley and I work with Microsoft 365 and Azure as an Engineer and Consultant. I enjoy writing technical content for you and engaging with the community. All opinions are my own.

Leave a Reply