SharePoint (2003 thru Online): M365 Tenant - SharePoint Online Storage Cleanup

Monday, February 23, 2026

M365 Tenant - SharePoint Online Storage Cleanup

Items in the SharePoint Online Recycle Bin, including both the first and second stages, contribute to the site's storage quota for 93 days. Storage space is only released once items are permanently deleted. 

If there is a retention policy, deleted items may be moved to the Preservation Hold Library, which also consumes storage. 

To free up space right away, delete items from the second-stage Recycle Bin, and then clean up the Preservation Hold Library.

The PowerShell script below is the most effective way to carry out these tasks across all site collections in the tenant.

#==================================================================================================
# Retention Exception Update + PHL Cleanup + Recycle Bin Cleanup (PnP)
#==================================================================================================

#------------------------------ CONFIG -------------------------------------
$PolicyName               = "Document Retention Policy"   # Purview retention policy name
$ExceptionsCsvPath        = "E:\AReports\RP_Sites_DEL_2026_3.csv"
$SitesCsvPath             = "E:\AReports\Sites_DEL_2026_3.csv"

$IPPSSessionUPN           = "spadmin@spadmins.onmicrosoft.com"
$PnPClientId              = "12a34567-f123-4567-890e-1cch23456789"

# Batch size for policy exception updates (helps manage large lists)
$PolicyBatchSize          = 50

# PHL paging size
$PHLPageSize              = 2000

# Recycle bin cleanup controls
$EnableRecycleBinCleanup  = $true
$RecycleBinRowLimit       = 0        # 0 = no limit; otherwise e.g. 10000 (PnP supports -RowLimit).

#------------------------------ SECTION 1: RETENTION POLICY EXCEPTIONS ---------------------------
Write-Host "=== Updating Retention Policy Exceptions ===" -ForegroundColor Cyan

# Load, normalize, and de-dupe site URLs from CSV
[array]$excludeSites = Import-Csv -Path $ExceptionsCsvPath |
    Select-Object -ExpandProperty URL |
    ForEach-Object { $_.Trim().TrimEnd("/") } |
    Where-Object { $_ } |
    Sort-Object -Unique

Write-Host "Loaded $($excludeSites.Count) exception site URLs from $ExceptionsCsvPath"

# Connect to Purview / Security & Compliance PowerShell
Connect-IPPSSession -UserPrincipalName $IPPSSessionUPN

# IMPORTANT:
# - Microsoft notes Set-RetentionCompliancePolicy triggers a full orgsync and recommends waiting for distribution between updates.
# - Static scoping limit: 100 SharePoint sites per retention policy when specifying sites.
# This script batches updates to reduce risk; adjust batching per your change control.

for ($i = 0; $i -lt $excludeSites.Count; $i += $PolicyBatchSize) {
    $end = [Math]::Min($i + $PolicyBatchSize - 1, $excludeSites.Count - 1)
    $batch = $excludeSites[$i..$end]

    try {
        Set-RetentionCompliancePolicy -Identity $PolicyName -AddSharePointLocationException $batch
        Write-Host "Added exception batch: $($i+1) - $($end+1)" -ForegroundColor Green
    }
    catch {
        Write-Host "FAILED adding exception batch: $($i+1) - $($end+1). Error: $($_.Exception.Message)" -ForegroundColor Red
    }
}

#------------------------------ SECTION 2: PHL CLEANUP + RECYCLE BIN CLEANUP ---------------------
Write-Host "`n=== PHL Cleanup + Recycle Bin Cleanup ===" -ForegroundColor Cyan

$sites = Import-Csv -Path $SitesCsvPath
Write-Host "Loaded $($sites.Count) sites from $SitesCsvPath"

# Check if Clear-PnPRecycleBinItem exists in the current PnP.PowerShell install
# PnP docs indicate this cmdlet is available in the Nightly release.
$hasClearRecycleCmd = $null -ne (Get-Command Clear-PnPRecycleBinItem -ErrorAction SilentlyContinue)

foreach ($site in $sites) {
    $siteUrl = $site.URL.Trim().TrimEnd("/")
    if (-not $siteUrl) { continue }

    Write-Host "'nProcessing site: $siteUrl" -ForegroundColor Yellow

    # Connect to the site
    try {
        Connect-PnPOnline -Url $siteUrl -Interactive -ClientId $PnPClientId -ErrorAction Stop
    }
    catch {
        Write-Host "Failed to connect to $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
        continue
    }

    # Check if Preservation Hold Library exists
    $phl = Get-PnPList -Identity "Preservation Hold Library" -ErrorAction SilentlyContinue
    if (-not $phl) {
        Write-Host "No Preservation Hold Library found at $siteUrl" -ForegroundColor DarkGray
    }
    else {
        Write-Host "Preservation Hold Library found at $siteUrl" -ForegroundColor Green

        # Retrieve items in pages idle large libraries (PnP supports -PageSize and -ScriptBlock pattern).
        $items = @()
        try {
            $items = Get-PnPListItem -List "Preservation Hold Library" -PageSize $PHLPageSize -ScriptBlock {
                param($pagedItems)
                $pagedItems.Context.ExecuteQuery()
            }
        }
        catch {
            Write-Host "Failed to list PHL items at $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
            $items = @()
        }

        if ($items.Count -gt 0) {
            Write-Host "$($items.Count) items found in Preservation Hold Library" -ForegroundColor Cyan

            foreach ($item in $items) {
                try {
                    Remove-PnPListItem -List "Preservation Hold Library" -Identity $item.Id -Force -ErrorAction Stop
                }
                catch {
                    Write-Host "Failed to delete PHL item ID $($item.Id): $($_.Exception.Message)" -ForegroundColor DarkYellow
                }
            }
        }
        else {
            Write-Host "No items found in Preservation Hold Library" -ForegroundColor DarkGray
        }
    }

    # Clear Recycle Bins (1st + 2nd stage) to free storage immediately.
    # Your internal recycle bin guidance notes both stages exist; second stage typically needs site collection admin.
    if ($EnableRecycleBinCleanup) {
        if (-not $hasClearRecycleCmd) {
            Write-Host "Clear-PnPRecycleBinItem cmdlet not found. Per PnP docs, it may require PnP.PowerShell Nightly. Skipping recycle bin cleanup." -ForegroundColor DarkYellow
        }
        else {
            try {
                if ($RecycleBinRowLimit -gt 0) {
                    # -RowLimit supported by PnP.
                    Clear-PnPRecycleBinItem -All -Force -RowLimit $RecycleBinRowLimit
                }
                else {
                    # Clears all items; -Force suppresses confirmation.
                    Clear-PnPRecycleBinItem -All -Force
                }
                Write-Host "Recycle bins cleared for: $siteUrl" -ForegroundColor Green
            }
            catch {
                Write-Host "Failed to clear recycle bins for $siteUrl : $($_.Exception.Message)" -ForegroundColor Red
            }
        }
    }
}

Write-Host "`n=== DONE ===" -ForegroundColor Cyan

No comments:

Post a Comment