Several of the Microsoft 365 resources are accessible through the Microsoft Graph REST API. As adoption grows, Microsoft introduces more resources and more APIs. But with this increased adoption, customers are generating more and more API requests. And when too many requests are generated too quickly, Microsoft will throttle. This results in a series of 429 errors. Why? Because Microsoft is protecting their infrastructure…
However, it is possible to reduce the likelihood of throttling. Microsoft’s recommendation and best practice is to combine multiple requests into batches. This is the practice of bundling requests together, thus making applications more efficient. Rather than pinging Microsoft for a bunch of individual asks, batching combines these smaller asks into large asks. When building these batches, nest the non-GET properties of each request within its body.
Batching Example:
Marketing has a requirement to create 8 folders in their SharePoint Online sites. They will provide a list of site URLs, which are used to query each target site and library ID:
[System.Array] $listOfSites = @(
"/sites/Test001", "/sites/Test002",
"/sites/Test003", "/sites/Test004",
"/sites/Test005"
)
[System.String] $targetLibr = "Documents"
[System.Array] $listOfFolders = @(
"New Folder 001", "New Folder 002", "New Folder 003",
"New Folder 004", "New Folder 005", "New Folder 006",
"New Folder 007", "New Folder 008"
)
Typically, these folders are created individually in a for-loop. But counting the total requests:
- 10 total requests per site.
- 1 GET request to return the siteID,
- 1 GET request to return the librID,
- 8 POST requests to create each folder.
for($i = 0; $i -lt $listOfFolders.Count; $i++) {
$JSON = @{
"name" = $listOfFolders[$i]
"folder" = @{}
"@microsoft.graph.conflictBehavior" = "rename"
} | ConvertTo-Json
$JSON | Out-Host
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/sites/$($siteID)/drives/$($librID)/root/children" `
-Headers (Get-GraphAPIHeader) `
-Body $JSON `
-Method Post `
-ContentType "application/json" `
-UseBasicParsing
}
Alternatively, these requests could be bundled into a single batch request. Build an array of objects and pass them to the /$batch endpoint. Optimized a bit, this results in:
- 3 total requests per site.
- 1 GET request to return the siteID.
- 1 GET request to return the librID,
- 1 POST request to submit the batch.
NOTE: Batches are limited to 20 requests per batch.
$requests = @()
for($i = 0; $i -lt $listOfFolders.Count; $i++) {
$requests += @{
"url" = "/sites/$($siteID)/drives/$($librID)/root/children"
"method" = "POST"
"id" = ($requests.Count + 1)
"body" = @{
"name" = $listOfFolders[$i]
"folder" = @{}
}
"headers" = @{
"Content-Type" = "application/json"
}
}
}
$JSON = @{
"requests" = $requests
} | ConvertTo-Json -Depth 5
$JSON | Out-Host
Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/`$batch" `
-Headers (Get-GraphAPIHeader) `
-Body $JSON `
-Method POST `
-ContentType "application/json" `
-UseBasicParsing
FYI: This could be optimized a bit more. Create a batch of GETs to return all the site IDs, then create a batch of GETs to return all the library IDs.
NOTE: Batch requests are indexed using their id property and submitted as POSTs.
Moving on, each folder batch submitted to Microsoft will look something like this:
{
"requests": [
{
"body": {
"folder": {},
"name": "Folder 001"
},
"method": "POST",
"url": "/sites/contoso.sharepoint.com,076bf298-1234-49b8-9658-9c9796e3623c,7e80afae-d0f6-418a-87f4-773b9fe11381/drives/b!mPJrB6couEmWWJyXluNiPK6vgH720IpBh_R3O5_hE4H0-fEDWHMKQ6oR_pSl_tuw/root/children",
"id": "1",
"headers": {
"Content-Type": "application/json"
}
},
{
"body": {
"folder": {},
"name": "Folder 002"
},
"id": "2",
...
},
{
"body": {
"folder": {},
"name": "Folder 003"
},
"id": "3",
...
},
...
...
...
]
}
FYI: Batches can contain a mix of GET, POST, PATCH, and DELETE requests. Just assign the Method property of each request accordingly.
Reviewing a successful submission, the response object includes a status value for each indexed request. It is possible that some requests succeeded while others failed. To check, iterate the response object and look for successful statuses; code 201. Also, requests aren’t guaranteed to process in order:

Additionally, each successful response returns a body object with the folder properties:

Nested a bit, more body object properties are found, including folder name, webUrl, size, etc:

Lastly, open one of the target sites and confirm that the folders were created:

Conclusion:
Batching is more efficient for a number of reasons. But most importantly, it greatly reduces the total number of requests sent to Microsoft. Imagine doing this for thousands of sites! No one wants to be throttled and put in timeout…
“It comes down to this: black people were stripped of our identities when we were brought here, and it’s been a quest since then to define who we are.”
Shelton Jackson Lee
#BlackLivesMatter
Pingback: Graph API: Batch Delete SPO List Items | console.log('Charles');