All scripts
Microsoft 365 501

OneDrive Recycle Bin Storage Report

Scans every user's OneDrive recycle bin and reports how much space deleted items are taking up, so you can see who is quietly hoarding deleted files instead of guessing why OneDrive storage keeps climbing.

Get-OneDriveRecycleBinReport.ps1
<#
.SYNOPSIS
    Reports how much space is used in each user's OneDrive recycle bin.

.DESCRIPTION
    Connects to SharePoint Online as a tenant admin, finds every OneDrive
    site collection, then checks the first and second stage recycle bins
    for each one. Useful when OneDrive storage keeps climbing and nobody
    can say why, since deleted files sit in the recycle bin for up to 93
    days before they actually free up space.

.AUTHOR
    Shehryar Hassan

.EXAMPLE
    .\Get-OneDriveRecycleBinReport.ps1 -AdminUrl "https://contoso-admin.sharepoint.com" -MinimumSizeMB 500
    Lists every OneDrive whose combined recycle bin size is 500 MB or more.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$AdminUrl,

    [Parameter(Mandatory = $false)]
    [double]$MinimumSizeMB = 0,

    [Parameter(Mandatory = $false)]
    [string]$ExportPath = ".\OneDriveRecycleBinReport.csv"
)

if (-not (Get-Module -ListAvailable -Name PnP.PowerShell)) {
    Write-Error "PnP.PowerShell module not found. Install it first with: Install-Module PnP.PowerShell -Scope CurrentUser"
    return
}

Import-Module PnP.PowerShell

Write-Host "Connecting to $AdminUrl ..."
Connect-PnPOnline -Url $AdminUrl -Interactive

$oneDriveSites = Get-PnPTenantSite -IncludePersonalSite $true -Filter "Url -like '-my.sharepoint.com/personal/'"

Write-Host "Found $($oneDriveSites.Count) OneDrive sites. Checking recycle bins, this can take a while."

$results = @()
$counter = 0

foreach ($site in $oneDriveSites) {
    $counter++
    Write-Progress -Activity "Checking OneDrive recycle bins" -Status $site.Url -PercentComplete (($counter / $oneDriveSites.Count) * 100)

    try {
        Connect-PnPOnline -Url $site.Url -Interactive -WarningAction SilentlyContinue

        $firstStage = Get-PnPRecycleBinItem -RowLimit 5000
        $secondStage = Get-PnPRecycleBinItem -SecondStage -RowLimit 5000

        $firstStageSizeMB = [math]::Round((($firstStage | Measure-Object -Property Size -Sum).Sum / 1MB), 2)
        $secondStageSizeMB = [math]::Round((($secondStage | Measure-Object -Property Size -Sum).Sum / 1MB), 2)
        $totalSizeMB = [math]::Round(($firstStageSizeMB + $secondStageSizeMB), 2)

        if ($totalSizeMB -ge $MinimumSizeMB) {
            $results += [PSCustomObject]@{
                OneDriveUrl        = $site.Url
                Owner              = $site.Owner
                FirstStageItems    = $firstStage.Count
                FirstStageSizeMB   = $firstStageSizeMB
                SecondStageItems   = $secondStage.Count
                SecondStageSizeMB  = $secondStageSizeMB
                TotalRecycleBinMB  = $totalSizeMB
            }
        }
    }
    catch {
        Write-Warning "Could not check $($site.Url): $($_.Exception.Message)"
    }
}

$results = $results | Sort-Object TotalRecycleBinMB -Descending
$results | Export-Csv -Path $ExportPath -NoTypeInformation

Write-Host "Done. $($results.Count) OneDrive sites met the $MinimumSizeMB MB threshold."
Write-Host "Report saved to $ExportPath"

$results | Format-Table -AutoSize

Read it before you run it, and test in a safe tenant first.