PnP Batch versus Microsoft Graph Batch in PowerShell to add/delete 3k items
Following on previous blog PnP Batch Add or Delete items from very large list
and reading about the post fastest way to create SharePoint List Items by Waldek Mastykarz, I decided to try using Microsoft Graph batch in PowerShell. I found the article Calling the Microsoft Graph with PnP PowerShell by Paul Bullock useful to get started. Using PnP Batch with retry mechanisms still took up to 4 hours to create 300k items and up to 8 hours to delete 300 k items. I thought Microsoft Graph can handle huge volume of requests and could help with my scenario. Unfortunately, at random intervals I was getting errors like
"Invoke-WebRequest : The underlying connection was closed: A connection that was expected to be kept alive was closed by the server"



Microsoft Graph batch script
$action = Read-Host "Enter the action you want to perform, e.g. Add or Delete"
$siteUrl = "https://contoso.sharepoint.com/sites/Team1"
$listName = "TestDemo"
$clientId = "00000000-0000-0000-0000-000000000000"
$thumbprint = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
#connect with application permissions
Connect-PnPOnline -ClientId $clientId -Thumbprint $thumbprint -Tenant "contoso.onmicrosoft.com" -Url $siteUrl
write-host $("Start time " + (Get-Date))
$startTime = Get-Date
#site and list details
$siteID = (Get-PnPSite -Includes Id).Id
$listID = (Get-PnPList $listName).Id
$Total = 3000
$batchSize = 20
#bearer token for batch request
$token = Get-PnPGraphAccessToken
$Stoploop = $false
$Retrycount = 0
$requests = @()
$header = @{ "Content-Type" = "application/json" }
do {
try {
if($action -eq "Add")
{
$lst = Get-PnPList -Identity $listName
if($lst.ItemCount -lt $Total)
{
$startInc = $lst.ItemCount
$itemsCountToCreate = $Total - $startInc
for($i=$startInc;$i -lt ($Total);$i++)
{
$request = @{
id = $i
method = "POST"
url = "/sites/$siteID/lists/$listID/items/"
body = @{ fields = @{ Title = "Test $i" } }
headers = $header
}
$requests += $request
if($requests.count -eq $batchSize -or $requests.count -eq $itemsCountToCreate)
{
$batchRequests = @{
requests = $requests
}
#IMPORTANT: use -Deph parameter
$batchBody = $batchRequests | ConvertTo-Json -Depth 4
#send batch request
$response = Invoke-WebRequest -Method Post -Uri 'https://graph.microsoft.com/v1.0/$batch' -Headers @{Authorization = "Bearer $($token)" } -ContentType "application/json" -Body $batchBody -ErrorAction Stop
$StatusCode = $Response.StatusCode
# This will only execute if the Invoke-WebRequest is successful.
#write-host $("$StatusCode response for adding 20")
#reset batch item counter and requests array
$requests = @()
$itemsCountToCreate = $itemsCountToCreate - $batchSize
}
}
}
$lst = Get-PnPList -Identity $listName
$Stoploop = $true
}
if($action -eq "Delete")
{
$requests = @()
$listItems= Get-PnPListItem -List $listName -Fields "ID" -PageSize 1000
$itemCount = $listItems.Count
for($i=$itemCount-1;$i -ge 0;$i--)
{
$itemId = $listItems[$i].Id
$request = @{
id = $i
method = "DELETE"
url = "/sites/$siteID/lists/$listID/items/$itemId"
headers = $header
}
$requests += $request
if($requests.count -eq $batchSize -or $requests.count -eq $itemCount)
{
$batchRequests = @{
requests = $requests
}
#IMPORTANT: use -Deph parameter
$batchBody = $batchRequests | ConvertTo-Json -Depth 4
#send batch request
$response = Invoke-WebRequest -Method Post -Uri 'https://graph.microsoft.com/v1.0/$batch' -Headers @{Authorization = "Bearer $($token)" } -ContentType "application/json" -Body $batchBody
#$StatusCode = $Response.StatusCode
#write-host $("$StatusCode response for deleting 20")
#reset batch item counter and requests array
$requests = @()
$itemCount = $itemCount - $batchSize
}
}
}
$Stoploop = $true
}
catch {
if ($Retrycount -gt 3){
Write-Host "Could not send Information after 3 retrys."
$Stoploop = $true
}
else {
Write-Host "Could not send Information retrying in 30 seconds..."
write-host $("Time error happened " + (Get-Date))
Write-host $("$_.Exception.Message") -ForegroundColor Red
Start-Sleep -Seconds 30
Connect-PnPOnline -ClientId $clientId -Thumbprint $thumbprint -Tenant "contoso.onmicrosoft.com" -Url $siteUrl
$token = Get-PnPGraphAccessToken
$Retrycount = $Retrycount + 1
}
}
}
While ($Stoploop -eq $false)
$endTime = Get-Date
$totalTime = $endTime - $startTime
write-host "Total script run time: $($totalTime.Hours) hours, $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" -ForegroundColor Cyan
PnP batch script
$action = Read-Host "Enter the action you want to perform, e.g. Add or Delete"
$siteUrl = "https://contoso.sharepoint.com/sites/Team1"
$listName = "TestDemo"
$ErrorActionPreference="Stop"
Connect-PnPOnline –Url $siteUrl -interactive
$Total = 30000
$Stoploop = $false
[int]$Retrycount = "0"
write-host $("Start time " + (Get-Date))
$startTime = Get-Date
do {
try {
if($action -eq "Add")
{
$lst = Get-PnPList -Identity $listName
if($lst.ItemCount -lt $Total)
{
$startInc = $lst.ItemCount
while($lst.ItemCount -lt $Total)
{
$batch = New-PnPBatch
#perform in increment of 1000 until 300k is reached
if($startInc+1000 -gt $Total)
{
$endNu = $Total
}
else
{
$endNu = $startInc+1000
}
for($i=$startInc;$i -lt ($endNu);$i++)
{
Add-PnPListItem -List $listName -Values @{"Title"="Test $i"} -Batch $batch
}
Invoke-PnPBatch -Batch $batch
$lst = Get-PnPList -Identity $listName
}
}
}
if($action -eq "Delete")
{
$listItems= Get-PnPListItem -List $listName -Fields "ID" -PageSize 1000
$itemIds = $lisItems | Foreach {$_.Id}
$itemCount = $listItems.Count
while($itemCount -gt 0)
{
$batch = New-PnPBatch
#delete in batches of 1000, if itemcount is less than 1000 , all will be deleted
if($itemCount -lt 1000)
{
$noDeletions = 0
}
else
{
$noDeletions = $itemCount -1000
}
for($i=$itemCount-1;$i -ge $noDeletions;$i--)
{
Remove-PnPListItem -List $listName -Identity $listItems[$i].Id -Batch $batch
}
Invoke-PnPBatch -Batch $batch
$itemCount = $itemCount-1000
}
}
Write-Host "Job completed"
$Stoploop = $true
}
catch {
if ($Retrycount -gt 3){
Write-Host "Could not send Information after 3 retrys."
$Stoploop = $true
}
else {
Write-Host "Could not send Information retrying in 30 seconds..."
Start-Sleep -Seconds 30
Connect-PnPOnline –Url $siteUrl -interactive
$Retrycount = $Retrycount + 1
}
}
}
While ($Stoploop -eq $false)
write-host $("End time " + (Get-Date))
$endTime = Get-Date
$totalTime = $endTime - $startTime
write-host "Total script run time: $($totalTime.Hours) hours, $($totalTime.Minutes) minutes, $($totalTime.Seconds) seconds" -ForegroundColor Cyan
I have run the scripts using Microsoft Graph batch and PnP Batch 3 times to add and delete 3k items and below are the results.
Microsoft Graph batch is faster to add and delete than PnP batch for 3k items.
However for a very large number of items, i.e. more than 10 k the results can be sporadic with Microsoft Graph batch unless I added a delay of few seconds after each batch operation which makes it less performant than using PnP batch.