Site icon Microsoft 365 for IT Pros

Primer: How to Use JSON Batching to Speed up Graph Processing

JSON batching speeds up Graph processing.
Advertisements

Speed Up Large-Scale Microsoft 365 Updates with Graph Batch Processing

JSON batching is one of the most effective ways to improve the performance of Graph-based automation in large Microsoft 365 tenants. Speed is gained by combining up to 20 requests into a single JSON payload and a single HTTP call. The Graph accepts the JSON request and processes each request in the batch. Because only a single request is sent over the network to the Graph batch endpoint, this technique is faster and more efficient than sending individual requests to update the twenty objects that can be in a batch.

JSON batching isn’t unique to Microsoft Graph. Similar mechanisms exist across the industry, including Google APIs, Stripe, Shopify, and OData-based services. In every case the objective is the same: reduce network round trips, improve performance, and make it easier to process large numbers of operations efficiently. Microsoft Graph JSON batching follows a well-established pattern used by many large-scale cloud platforms

Batching is useful whenever large numbers of Graph operations must be executed. Typical examples include:

Fetching Data with JSON Batching

Although JSON batching is often used to speed up updates for large numbers of objects, it can also be used to retrieve information faster than the standard Graph paginated mechanism. Graph pagination usually restricts the number of objects that can be retrieved at one time in a range from 10 to 999 depending on the endpoint.

JSON batching does not change how Graph pagination works. If a batched request returns more data than can fit in a single page, the Graph includes an @odata.nextLink value in the response for that request. Applications must continue to follow @odata.nextLink values until no additional pages remain. Batching reduces the number of HTTP round trips required to execute requests, but it does not eliminate the need to handle Graph pagination

Understanding an Example of Graph Batch Processing

An example is worth a thousand words, or so it’s said. Let’s go through an example of creating batches to update Entra ID user objects. To run this code, you’ll need to sign into an interactive Microsoft Graph PowerShell SDK session with the User.ReadWrite.All permission. To update properties for accounts other than the signed-in account, the signed-in account must hold a suitable Entra ID role, such as User Administrator.

The first thing to do is fetch some user accounts to process. This command fetches all member accounts in the tenant. Remember to use an efficient server-side filter to find the target objects. Batching speeds things up, but it can’t compensate for inefficiencies in object retrieval, especially in larger tenants.

# Get a set of users to process. We only need the identifier and displayname 
[array]$Users = Get-MgUser -All -Filter "userType eq 'member'" -Property Id, DisplayName, CompanyName, OfficeLocation | Select-Object Id, DisplayName, CompanyName, OfficeLocation

Write-Host "Found $($users.Count) users to update"

Now we prepare to update the company name and office location properties for all accounts by defining new values for the properties. It’s possible that the set of fetched accounts contains some objects that already have the new property values. To speed things up, these accounts should be removed and not submitted for processing.

# Define the new company name
$newCompanyName = "Office 365 Maestro (UK) Limited"
$newOfficeLocation = "London"

$Users = $Users | Where-Object {$_.CompanyName -ne $newCompanyName -or $_.OfficeLocation -ne $newOfficeLocation}
If (!$Users) {
   Write-Host "No users need to be updated"
   Exit
}

We can now calculate how many batches are needed to process the set of user accounts by dividing the count of accounts by 20 (the maximum that can be in a batch).

# The Graph batch endpoint allows max 20 requests per batch. Figure out how
# many batches are needed
$BatchSize = 20
$Batches = [Math]::Ceiling($Users.Count/$BatchSize)

Submitting Batches for Processing

We now create a loop to send batches of 20 user accounts to the Graph batch endpoint. Submission occurs by submitting a POST request to the endpoint with a request body containing details of the work to be done by the batch. In this case, the request body contains the account identifier, the properties to be updated, and the HTTP action (PATCH to update).

Each request inside a batch must have an identifier stored as a string value. Microsoft Graph uses the identifier to correlate responses with requests. The identifier only needs to be unique within the current batch. In this case, a simple counter is used for a batch identifier, but you can use any string you like.

The code uses the Invoke-MgGraphRequest cmdlet to submit each batch for processing. The Graph evaluates requests within a batch individually. A successful batch response does not guarantee that every request in the batch succeeded, which is why the code checks the status returned for each operation.

The code flags any HTTP status code of 400 or higher. In production, you would likely implement more sophisticated handling, including retries for transient failures such as 429 (Too Many Requests).

For ($i = 0; $i -lt $Batches; $i++) {

    $Start = $i * $batchSize
    $End = [Math]::Min($Start + $BatchSize - 1, $Users.Count - 1)
    $CurrentBatch = $Users[$Start..$End] | Select-Object Id, displayName

    # Build the batch request body - each entry needs a unique "id",
    # the target method/url, and the body of the PATCH
    $batchRequests = [System.Collections.Generic.List[object]]::new()
    $counter = 0

    ForEach ($user in $currentBatch) {
        $counter++
        $batchRequest = [PSCustomObject][Ordered]@{
            id      = "$counter"
            method  = "PATCH"
            url     = "/users/$($user.Id)"
            headers = @{ "Content-Type" = "application/json" }
            body    = @{ companyName = $newCompanyName 
                         officeLocation = $newOfficeLocation }
        }
        $BatchRequests.Add($BatchRequest)
    }

    $BatchBody = @{ Requests = $BatchRequests }

    Try {
        $Response = Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/`$batch" -Body ($batchBody | ConvertTo-Json -Depth 10) -ErrorAction Stop
        # Check each individual response inside the batch for errors
        ForEach ($Result in $Response.responses) {
            If ($Result.status -ge 400) {
                $FailedUser = $CurrentBatch[[int]$result.id - 1]
                Write-Warning "Failed to update $($FailedUser.DisplayName): Status $($Result.status) - $($Result.body.error.message)"
            }
        }
        Write-Host "Batch $($i + 1) of $Batches completed"
    } Catch {
        Write-Error "Batch $($i + 1) failed entirely: $_"
    }

    # Small pause to avoid hammering the throttling limits between batches
    Start-Sleep -Milliseconds 200
}

Write-Host "Company name update complete."

You can download the script code from the Microsoft 365 for IT Pros GitHub repository.

Individual Graph endpoints impose their own throttling limits, and it’s important to realize that batching is not a way to overcome or work around these limits. The code includes a small pause between batch submissions to reduce the likelihood of throttling. Production code would normally implement retry logic based on Retry-After values returned by the Graph endpoint.

What’s in a Batch for Processing by the Graph

A batch created from the set of fetched user accounts looks like that shown below (remember, only two properties per account were fetched). Only the account identifier is needed to drive processing. Always use an immutable property like an account identifier in batches. The display name is present in case an error occurs and the code needs to advise which account had a problem.

Id                                   DisplayName
--                                   -----------
d9390586-f7c4-4c38-8cc6-3ae7a51597fa James Hoover
3929b545-0737-4109-8aa5-a079159ba3cc Warren Gatland
a732c057-8947-4f74-8e6e-35d6c8696720 Tricia Griffin (Champion)

If we examine the requests fed to the Graph endpoint, we see data like that shown below. You can see the relative Graph URL to access the data (/users/{id}) and the properties to update.

Name                           Value
----                           -----
id                             1
url                            /users/d9390586-f7c4-4c38-8cc6-3ae7a51597fa
body                           {[officeLocation, London], [companyName, Office 365 Maestro (UK) Limited]}
method                         PATCH
headers                        {[Content-Type, application/json]}
id                             2
url                            /users/3929b545-0737-4109-8aa5-a079159ba3cc
body                           {[officeLocation, London], [companyName, Office 365 Maestro (UK) Limited]}
method                         PATCH
headers                        {[Content-Type, application/json]}
id                             3
url                            /users/a732c057-8947-4f74-8e6e-35d6c8696720
body                           {[officeLocation, London], [companyName, Office 365 Maestro (UK) Limited]}
method                         PATCH
headers                        {[Content-Type, application/json]}

DependsOn

The Graph batch endpoint supports a special dependsOn property to control the sequencing of requests within a batch. For example, you can instruct the Graph not to execute request ABC3 until request ABC2 has completed successfully.

Graph Requests and Not Graph SDK Cmdlets

You’ll notice that Graph API request URLs are used in batches rather than Microsoft Graph PowerShell SDK cmdlets. JSON batching works at the HTTP layer, and batched requests must be expressed as Graph URLs and HTTP actions (GET, POST, PATCH, DELETE). By comparison, Graph SDK cmdlets generate individual REST requests that aren’t suitable for batching.

You can always find the Graph API request for a Graph SDK cmdlet by running it with the Debug parameter and use the request as shown by debug for batch processing. And as you see here, I use the Invoke-MgGraphRequest cmdlet to submit batches.

Batching isn’t Difficult

Most Microsoft 365 administrators whom I have met are unaware of the power of JSON batching. That being said, there’s a ton of other knowledge that needs to be acquired before plunging into JSON batching. Once you’ve mastered the basics of Microsoft Graph interactions, JSON batching should be one of the next techniques to learn. It’s a simple concept that can deliver substantial performance improvements in larger Microsoft 365 tenants.


Need help to write and manage PowerShell scripts for Microsoft 365, including Azure Automation runbooks? Get a copy of the Automating Microsoft 365 with PowerShell eBook, available standalone or as part of the Microsoft 365 for IT Pros eBook bundle.

Exit mobile version