If you’ve looked at your SharePoint storage report lately and wondered where all the space went, check version history. Depending on your tenant’s configuration — up to 500 major versions, or Microsoft’s newer “automatic” model with no fixed cap — there’s no expiration by default. Every save, every autosave from co-authoring, every edit creates a version that sticks around forever.

We found libraries where version history was consuming 3-4x the space of the current files. Co-authoring makes it worse — a one-hour session with multiple editors can generate dozens of versions. Multiply that across hundreds of sites and it adds up fast.

Microsoft added tenant-level version controls in the SharePoint Admin Center (Settings > Version history limits), which works for broad policy. But we needed per-library control baked into our site provisioning pipeline — applied consistently whether a site was created today or two years ago, without relying on someone remembering to toggle an admin setting.

We needed a consistent policy: 100 major versions, minor versions enabled, and automatic expiration after 60 days. Applied to every site in the tenant — both existing sites and every new site as it’s provisioned. Here’s how we did it.


The Versioning Policy

We reviewed storage reports and asked the business how far back they’d ever actually needed to roll back. The answer was almost always days, not months. We landed on:

SettingValueRationale
Major versions100Covers months of active editing, even on busy documents
Minor versionsEnabledSupports draft/publish workflows
Version expiration60 daysAuto-trims anything older than two months

The 60-day expiration is the key lever. It bounds version history regardless of edit frequency. Both major and minor versions are subject to the policy — anything older than 60 days is eligible for cleanup.


Applying Versioning at Provisioning Time

Our sites are provisioned via an Azure Function queue trigger that creates the site, applies a Patterns and Practices (PnP) template, copies template folders, and configures versioning. Here’s the relevant section:

# Enable versioning on the Documents library
Write-Host "Enabling versioning on 'Documents' library."
Set-PnPList -Identity "Documents" `
    -EnableVersioning $true `
    -MajorVersions 100 `
    -EnableMinorVersions $true `
    -Connection $newSiteConnection

Write-Host "Setting expiration for versions in 'Documents' library to 60 days."
Set-PnPList -Identity "Documents" `
    -ExpireVersionsAfterDays 60 `
    -Connection $newSiteConnection

Note the two separate Set-PnPList calls. Combining -ExpireVersionsAfterDays with the other versioning parameters in a single call can produce inconsistent results depending on your PnP PowerShell version. We learned that the hard way — split them and it’s reliable.


Remediating Existing Sites

New sites are handled, but existing sites still had default settings and months of accumulated version history. We built a bulk remediation script that reads a site inventory CSV and applies the same policy:

Import-Module PnP.PowerShell

# Path to the CSV file containing site inventory
$csvFilePath = "sites.csv"

# Read the CSV file
$sites = Import-Csv -Path $csvFilePath

# Iterate through each active site
foreach ($site in $sites) {
    if ($site.Status -eq 'C' -and -not [string]::IsNullOrWhiteSpace($site.SiteUrl)) {
        try {
            $siteUrl = $site.SiteUrl
            Connect-PnPOnline -Url $siteUrl -ClientId "your-app-registration-client-id"

            # Apply versioning policy
            Set-PnPList -Identity "Documents" `
                -EnableVersioning $true `
                -MajorVersions 100 `
                -EnableMinorVersions $true

            Set-PnPList -Identity "Documents" `
                -ExpireVersionsAfterDays 60

            Write-Host "Successfully updated site: $siteUrl" -ForegroundColor Green
        }
        catch {
            Write-Host "Failed to update site: $siteUrl. Error: $_" -ForegroundColor Red
        }
        finally {
            Disconnect-PnPOnline
        }
    }
}

The CSV tracks every site with a status column — C for active, R for retired. The script skips retired sites and empty URLs, so it’s safe to run against the full inventory.


What Happens When Expiration Kicks In

-ExpireVersionsAfterDays 60 doesn’t delete old versions immediately — SharePoint handles it as a background job. A few things to know:

  • Background timer job. Versions older than 60 days become eligible for deletion, but actual cleanup can take days on large libraries.
  • Current version is never expired. Only historical versions are affected.
  • Recycle bin delay. Deleted versions go to the recycle bin (93 days total across first and second stage). True storage recovery happens after the recycle bin clears.

If you need to reclaim storage immediately:

Clear-PnPRecycleBinItem -All -Force -Connection $siteConnection

Not reversible. We ran this selectively on sites with the highest storage pressure, not tenant-wide.


Monitoring the Results

After rolling this out, we tracked storage in SharePoint Admin Center. Within a few weeks, total tenant storage dropped as background expiration worked through the backlog. The bigger win was that storage growth flattened — no more unbounded accumulation.


Lessons Learned

Split the Set-PnPList calls. Versioning and expiration in separate calls avoids parameter combination edge cases.

Run remediation quarterly. Settings can drift — someone changes a library manually, or a new library gets created outside the pipeline. Re-running the bulk script catches it.

Watch the recycle bin. Expired versions go to the recycle bin, not into thin air. If you’re trying to reclaim storage urgently, clear the recycle bins too.

Communicate before you enforce. We notified users that versions older than 60 days would be automatically cleaned up. Nobody complained — most didn’t know versioning existed. But it’s good governance to communicate before changing retention behavior.


Wrapping Up

SharePoint versioning is one of those features that works against you when left on autopilot. The defaults are safe but wasteful. By enforcing a consistent policy — 100 major versions with 60-day expiration — across every site in the tenant, we reduced storage consumption significantly and eliminated a growing cost problem.

The approach is two-pronged: apply the policy automatically when new sites are provisioned, and run a bulk remediation script to bring existing sites in line. Both use PnP PowerShell’s Set-PnPList, and both can run unattended.

If your tenant storage report has been creeping upward and you’re not sure why, check your version history. Chances are that’s where the space is going. And if you’re managing versioning across a tenant differently — or have hit edge cases with ExpireVersionsAfterDays — I’d like to hear about it in the comments.

About the Author

Developer, Designer, Thinker, Problem Solver, Office Servers and Services MVP, & Collaboration Director @ SoHo Dragon.

View Articles