Veeam v13: Capacity Planning, Repository Management, and Job Hygiene

Veeam v13 Capacity Planning Repository Management Job Hygiene PowerShell Veeam ONE

Veeam v13 Series | Component: VBR v13, Veeam ONE | Audience: Hands-on Sysadmins, Enterprise Architects

Most Veeam environments are deployed correctly and then left to drift. Six months later, repositories are filling unexpectedly, backup windows are creeping, and nobody can tell you how many VMs are actually protected because the job list has grown into something nobody fully understands. This isn't a Veeam problem. It's an operational hygiene problem. Veeam gives you all the tools to prevent it. Most teams just don't run them.

This article covers the full operational picture: how to size repositories accurately from first principles, how backup chain type affects long term storage growth, how to find and clean up job sprawl and orphaned restore points, what Veeam ONE reports to run on a weekly cadence, and a PowerShell health digest script that gives you a single output covering everything worth knowing about your environment's state. This is the article for keeping things running clean after the initial deployment.


1. Repository Capacity Planning from First Principles

The Veeam capacity calculator at veeam.com/calculators is the right starting point for new deployments. But you need to understand what it's calculating to use it correctly and to explain the numbers to anyone who questions them. The official formula from the Veeam best practices guide is:

Repository sizing formula (forward incremental with synthetic fulls)
Repository Space = (Full Backup Size + (Incremental Size * Retention Days)) * 1.25 headroom
                    -------------------------------------------------------------------
                                      Data Reduction Ratio

Where:
  Full Backup Size    = Source data size / Data Reduction Ratio
  Incremental Size    = Source data * Daily Change Rate / Data Reduction Ratio
  Retention Days      = Number of restore points to keep
  Data Reduction      = 2:1 conservative estimate (50% compression + dedup)
                        3:1 typical for mixed workloads
                        1:1 for pre-compressed data (video, encrypted VMs)
  1.25 headroom       = Required for synthetic full creation workspace

Example: 20 TB source, 3% daily change, 14-day retention, 2:1 reduction, ReFS/XFS
  Full backup size:    20 TB / 2 = 10 TB
  Daily incremental:   20 TB * 0.03 / 2 = 300 GB
  14-day incrementals: 300 GB * 14 = 4.2 TB
  Raw total:           10 TB + 4.2 TB = 14.2 TB
  With 1.25 headroom:  14.2 TB * 1.25 = 17.75 TB

  On ReFS or XFS with Fast Clone, the synthetic full doesn't consume
  additional space beyond references. The 1.25x headroom drops to 1.1x.
  Adjusted: 14.2 TB * 1.1 = 15.6 TB

Change Rate Assumptions by Workload Type

The change rate is the number that blows up capacity estimates when people get it wrong. Using 2 to 3% across the board is fine for file servers and general application servers. It's not fine for environments with significant database workloads.

Workload TypeTypical Daily Change RateNotes
File servers1% to 3%Low change. Conservative 3% is safe.
Web / app servers2% to 5%Standard estimate. Use 5% if any transaction logging.
SQL Server (OLTP)10% to 30%Active transaction logs change constantly. Size separately from general VMs.
SQL Server (reporting)2% to 5%Read heavy. Lower change rate but large disk footprint.
Exchange / mail5% to 10%Log files. Size with 10% if log truncation is not configured.
VDI (persistent)3% to 8%User profile writes. Highly variable.
VDI (non-persistent)Near 0%Linked clones and instant clones reset on reboot. Often excluded from backup.
Encrypted VMsActual rateDedup ratio drops to near 1:1. Size on raw data size, not reduced.

If you have Veeam ONE in your environment, run the VM Change Rate Estimation report before sizing any new repository. It reads actual VMware change data from vCenter and gives you per-VM change rates based on observed behavior over the last 30 days. That's more accurate than any estimate.

ReFS and XFS: The Space Multiplier

Formatting your repository on ReFS (Windows) or XFS (Linux) with Fast Clone enabled changes the capacity math significantly. When Veeam creates a synthetic full backup using Fast Clone, it doesn't copy blocks. It creates block level references. A synthetic full on a standard NTFS repository consumes space equal to a full backup. The same synthetic full on ReFS or XFS consumes near zero additional space because the unchanged blocks are just referenced, not copied.

For a repository with weekly synthetic fulls and 14-day retention, that's the difference between needing space for 2 full backups plus 13 incrementals versus space for 1 full backup plus 13 incrementals. On a 10 TB full backup, that's roughly 10 TB of repository space you don't need if you're on ReFS or XFS. The Veeam best practices guide explicitly recommends XFS over ReFS for new Linux deployments due to maturity and stability. Both work. XFS is preferred.


2. Backup Chain Types and Long Term Storage Behavior

Your backup chain type determines how space gets consumed and reclaimed over time. Most teams set this once during initial job creation and never think about it again until repository space becomes a problem.

Forward Incremental with Synthetic Fulls (Recommended)

This is Veeam's recommended chain type. One full backup followed by daily incrementals. On a weekly schedule, Veeam creates a synthetic full from the existing incrementals and then starts a new incremental chain. Old restore points are pruned according to your retention policy. On ReFS or XFS, the synthetic full creation uses Fast Clone and doesn't consume significant extra space.

The workspace requirement during synthetic full creation is the one capacity surprise here. VBR needs temporary space equal to the size of the synthetic full being created, even on ReFS or XFS. That's the working area for the transformation. It's reclaimed after the job completes. Size your repository with enough headroom to absorb this temporary spike, or use the 1.25x multiplier in your sizing formula.

Forever Forward Incremental

No periodic fulls. One initial full backup followed by incrementals indefinitely. Retention is enforced by merging the oldest incremental into the full. This approach saves space on the repository because you never have two full backups coexisting. But the merge operation reads the oldest incremental and writes it into the full file, which generates significant I/O on the repository during the retention enforcement period. On standard storage this is barely noticeable. On deduplication appliances it's a real problem because the merge creates read-modify-write patterns that hurt dedup ratios.

Don't use forever forward incremental with deduplication appliances (ExaGrid, Data Domain, StoreOnce). Use forward incremental with synthetic fulls instead, which is a write-mostly workflow that dedup appliances are designed to handle efficiently.

Reverse Incremental

The most recent restore point is always a full backup. Each new backup rolls the full forward and converts the previous full into a reverse incremental. Instant VM recovery from the most recent point is fast because you're starting from a full. But reverse incremental is the most I/O intensive chain type on the repository during the backup window. It's not recommended for new deployments. If you've inherited an environment using reverse incremental and your backup windows or repository I/O are tight, migrating jobs to forward incremental with synthetic fulls will improve both.


3. SOBR: Performance Tier vs Capacity Tier Decisions

A Scale Out Backup Repository combines a performance tier (local disk, fast NVMe, or SAN) with a capacity tier (object storage or tape). Understanding when data moves between them and how to tune that movement is the difference between a SOBR that runs efficiently and one that generates constant offload job failures.

The Offload Trigger

VBR offloads data from the performance tier to the capacity tier based on two settings you configure per SOBR: an age threshold (move backups older than N days) and an operational mode (Move vs Copy). Move removes the data from the performance tier after it's confirmed written to capacity tier. Copy keeps it on both. Use Move unless you have a specific reason to keep redundant copies on both tiers.

The capacity tier offload job runs on a schedule set in the SOBR properties. By default it runs daily. If your environment has a tight backup window, stagger the offload schedule so it doesn't compete with active backup jobs. The offload job generates significant read I/O on the performance tier as it reads data to send to object storage.

What You Can't Offload

Not every backup chain type can use the capacity tier Move operation. The capacity tier Move only supports forward incremental with synthetic fulls. Forever forward incremental jobs can't use Move mode, only Copy. This is documented in the Veeam best practices guide and is a real constraint to account for before choosing forever forward incremental for any job whose data you intend to age to object storage.

Performance Tier Fill Rate

Watch the performance tier fill rate, not just the total SOBR capacity. If your performance tier fills before the offload job runs, new backup jobs fail with "insufficient free disk space" errors even though the SOBR as a whole has capacity available in object storage. The VBR console shows this if you look at the individual extent details, not just the SOBR summary line. A good operational target is keeping the performance tier below 70% utilization. When it regularly exceeds 80%, either add performance tier extents or reduce the offload age threshold to move data to the capacity tier sooner.


4. Job Sprawl and Backup Hygiene

Job sprawl is what happens after two or three years of organic growth: backup jobs created for projects that ended, jobs protecting VMs that were decommissioned, jobs with overlapping scope where the same VM is being backed up twice, and jobs created by administrators who've since left the organization with naming conventions nobody else understands.

It's not just aesthetic clutter. Duplicate protection consumes repository space twice. Jobs protecting decommissioned VMs consume backup scheduling slots and window time. Jobs with inconsistent retention create unpredictable repository growth. Cleaning it up is maintenance work, but it's maintenance work that directly translates to smaller repositories, shorter backup windows, and cleaner reporting.

Finding Duplicate Protection

PowerShell: Find VMs protected by more than one backup job
Connect-VBRServer -Server "vbr-server.domain.local"

$allJobObjects = @{}

Get-VBRJob | Where-Object { $_.JobType -eq 'Backup' } | ForEach-Object {
    $job = $_
    Get-VBRJobObject -Job $job | ForEach-Object {
        $vmName = $_.Name
        if (-not $allJobObjects.ContainsKey($vmName)) {
            $allJobObjects[$vmName] = [System.Collections.Generic.List[string]]::new()
        }
        $allJobObjects[$vmName].Add($job.Name)
    }
}

$duplicates = $allJobObjects.GetEnumerator() | Where-Object { $_.Value.Count -gt 1 }

if ($duplicates.Count -eq 0) {
    Write-Host "No VMs with duplicate job coverage found."
} else {
    Write-Host "VMs covered by multiple backup jobs: $($duplicates.Count)"
    Write-Host ""
    $duplicates | Sort-Object Key | ForEach-Object {
        Write-Host "  VM: $($_.Key)"
        $_.Value | ForEach-Object { Write-Host "    Job: $_" }
    }
}

Disconnect-VBRServer

Finding Jobs Protecting No VMs

PowerShell: Find backup jobs with zero active objects
Connect-VBRServer -Server "vbr-server.domain.local"

$emptyJobs = Get-VBRJob | Where-Object { $_.JobType -eq 'Backup' } | ForEach-Object {
    $job     = $_
    $objects = Get-VBRJobObject -Job $job
    if ($objects.Count -eq 0) {
        [PSCustomObject]@{
            JobName      = $job.Name
            Description  = $job.Description
            CreationTime = $job.CreationTime
            LastRun      = $job.LatestRunLocal
        }
    }
}

if (-not $emptyJobs) {
    Write-Host "No empty backup jobs found."
} else {
    Write-Host "Jobs with no protected objects: $($emptyJobs.Count)"
    $emptyJobs | Format-Table -AutoSize
}

Disconnect-VBRServer

Finding Orphaned Restore Points

Orphaned restore points are backup files on the repository for VMs that no longer exist anywhere in the backup job configuration. They accumulate when VMs are removed from jobs but not from the repository, or when VMs are decommissioned without anyone cleaning up the backup chain. VBR has a background retention process that handles this automatically if you've enabled the Remove deleted items data after setting on each job. If that setting isn't enabled, orphaned restore points accumulate indefinitely.

PowerShell: Report orphaned restore points (VMs in backups but not in any job)
Connect-VBRServer -Server "vbr-server.domain.local"

# Get all VM names currently in active backup jobs
$protectedVMs = Get-VBRJob | Where-Object { $_.JobType -eq 'Backup' } |
    ForEach-Object { Get-VBRJobObject -Job $_ } |
    Select-Object -ExpandProperty Name -Unique

# Get all VM names that have restore points in repositories
$backedUpVMs = Get-VBRBackup | ForEach-Object {
    Get-VBRRestorePoint -Backup $_ | Select-Object -ExpandProperty VmName -Unique
} | Sort-Object -Unique

# Find VMs with restore points but not in any active job
$orphaned = $backedUpVMs | Where-Object { $_ -notin $protectedVMs }

if ($orphaned.Count -eq 0) {
    Write-Host "No orphaned restore points found."
} else {
    Write-Host "VMs with orphaned restore points: $($orphaned.Count)"
    Write-Host "(These VMs have backup data on disk but aren't in any active job)"
    Write-Host ""
    $orphaned | Sort-Object | ForEach-Object { Write-Host "  $_" }
    Write-Host ""
    Write-Host "Review each entry. Enable 'Remove deleted items data after' on affected jobs"
    Write-Host "to allow VBR to clean up these restore points automatically."
}

Disconnect-VBRServer
Don't delete orphaned restore points manually by removing files from the repository filesystem. VBR won't know the files are gone and will log errors when it tries to reference them. Instead, enable the "Remove deleted items data after" setting on each job (set to 30 days is a reasonable default). VBR's background retention process will then clean up orphaned chains safely through its own catalog aware deletion process.

5. Veeam ONE Reports to Run Weekly

These four reports give you the operational visibility that VBR's own console doesn't surface easily. If you're not running them regularly, you're finding out about problems from job failures instead of from proactive monitoring.

Protected VMs

In Veeam ONE Reporter, find this report under Veeam Backup & Replication Reports. It shows every VM in your virtual infrastructure and whether it has a successful backup within your defined RPO. The key filter to use: set the RPO threshold to 25 hours (one full day plus a one hour buffer). VMs that fall outside that window appear in red. VMs that have never been backed up appear as unprotected. This is the report that tells you whether you actually have the coverage you think you have.

Run it Monday morning. The list of red VMs is your first operational action item for the week. Before you answer any other question about backup health, answer this one: are all protected VMs actually getting backed up?

Capacity Planning for Backup Repositories

Also in Veeam ONE Reporter. This report reads the growth trend from each backup repository and projects when each one will be full at the current growth rate. It tells you the projected date of 100% utilization and recommends how much additional storage to provision to extend that date by 90 days. Run this monthly. When any repository shows less than 90 days of runway, start the procurement process. Storage orders take time. Don't wait until you're at 85% capacity before acting.

Backup Job Statistics

Shows success rate, average duration, and average data processed per job over the selected time window. Sort by success rate ascending to find jobs that are failing intermittently but not failing consistently enough to generate an alert. A job at 85% success rate over 30 days is failing roughly every 7 runs. That's a real problem you might not know about if you're only looking at last night's results. Run this weekly with a 30-day window.

Orphaned VMs

Veeam ONE maintains a list of VMs that were previously in backup jobs but have since been removed, including VMs that drifted out of job scope due to vMotion to an unprotected host or cluster. This is different from the PowerShell orphaned restore points report above, which looks at storage. The Veeam ONE report looks at VM protection scope. Run it as part of your weekly hygiene check and treat any VM on the list as needing a decision: either add it back to a job or document why it's intentionally unprotected.


6. Weekly Health Digest Script

This script gives you a single text output covering everything worth reviewing at the start of each week: job success rates, repository utilization, unprotected VM count, and jobs approaching failure thresholds. Run it as a scheduled task on Monday morning and email the output to your operations team.

PowerShell: Weekly Veeam health digest
param(
    [string]$VBRServer    = "vbr-server.domain.local",
    [int]   $LookbackDays = 7,
    [int]   $RepoWarnPct  = 80,   # warn when repo exceeds this % full
    [int]   $SuccessWarn  = 90,   # warn when job success rate drops below this %
    [int]   $RPOHours     = 25    # hours before a VM is considered unprotected
)

Connect-VBRServer -Server $VBRServer
$cutoff = (Get-Date).AddDays(-$LookbackDays)
$report = [System.Text.StringBuilder]::new()

$report.AppendLine("=" * 60) | Out-Null
$report.AppendLine("VEEAM HEALTH DIGEST  $(Get-Date -Format 'yyyy-MM-dd HH:mm')") | Out-Null
$report.AppendLine("Server: $VBRServer  |  Window: last $LookbackDays days") | Out-Null
$report.AppendLine("=" * 60) | Out-Null

# --- REPOSITORY UTILIZATION ---
$report.AppendLine("`nREPOSITORY UTILIZATION") | Out-Null
$report.AppendLine("-" * 40) | Out-Null

Get-VBRBackupRepository | ForEach-Object {
    $repo    = $_
    $totalGB = [math]::Round($repo.Info.CachedTotalSpaceMb / 1024, 1)
    $freeGB  = [math]::Round($repo.Info.CachedFreeSpaceMb  / 1024, 1)
    $usedGB  = $totalGB - $freeGB
    $pctUsed = if ($totalGB -gt 0) { [math]::Round(($usedGB / $totalGB) * 100, 1) } else { 0 }
    $flag    = if ($pctUsed -ge $RepoWarnPct) { " *** WARNING ***" } else { "" }

    $report.AppendLine("  $($repo.Name)") | Out-Null
    $report.AppendLine("    Used: $usedGB GB / $totalGB GB  ($pctUsed%)$flag") | Out-Null
}

# --- JOB SUCCESS RATES ---
$report.AppendLine("`nJOB SUCCESS RATES (last $LookbackDays days)") | Out-Null
$report.AppendLine("-" * 40) | Out-Null

$sessions = Get-VBRBackupSession | Where-Object { $_.CreationTime -gt $cutoff }

Get-VBRJob | Where-Object { $_.JobType -eq 'Backup' } | Sort-Object Name | ForEach-Object {
    $job        = $_
    $jobSess    = $sessions | Where-Object { $_.JobName -eq $job.Name }
    $total      = $jobSess.Count
    $succeeded  = ($jobSess | Where-Object { $_.Result -eq 'Success' }).Count
    $warnings   = ($jobSess | Where-Object { $_.Result -eq 'Warning' }).Count
    $failed     = ($jobSess | Where-Object { $_.Result -eq 'Failed'  }).Count
    $rate       = if ($total -gt 0) { [math]::Round(($succeeded / $total) * 100, 0) } else { 0 }
    $flag       = if ($total -gt 0 -and $rate -lt $SuccessWarn) { " *** BELOW THRESHOLD ***" } else { "" }

    if ($total -gt 0) {
        $report.AppendLine("  $($job.Name)") | Out-Null
        $report.AppendLine("    Runs: $total  |  OK: $succeeded  Warn: $warnings  Fail: $failed  |  Rate: $rate%$flag") | Out-Null
    }
}

# --- UNPROTECTED VMs ---
$report.AppendLine("`nVM PROTECTION STATUS") | Out-Null
$report.AppendLine("-" * 40) | Out-Null

$rpoCutoff      = (Get-Date).AddHours(-$RPOHours)
$protectedNames = Get-VBRJob | Where-Object { $_.JobType -eq 'Backup' } |
    ForEach-Object { Get-VBRJobObject -Job $_ } |
    Select-Object -ExpandProperty Name -Unique

$recentlyBacked = Get-VBRBackup | ForEach-Object {
    Get-VBRRestorePoint -Backup $_ |
        Where-Object { $_.CreationTime -gt $rpoCutoff } |
        Select-Object -ExpandProperty VmName
} | Sort-Object -Unique

$notRecentlyBacked = $protectedNames | Where-Object { $_ -notin $recentlyBacked }

$report.AppendLine("  In-scope VMs (in a job):          $($protectedNames.Count)") | Out-Null
$report.AppendLine("  Backed up within ${RPOHours}h:            $($recentlyBacked.Count)") | Out-Null
$report.AppendLine("  Missing recent backup (RPO miss): $($notRecentlyBacked.Count)") | Out-Null

if ($notRecentlyBacked.Count -gt 0) {
    $report.AppendLine("  *** RPO MISSES ***") | Out-Null
    $notRecentlyBacked | Sort-Object | ForEach-Object {
        $report.AppendLine("    $_") | Out-Null
    }
}

# --- ORPHANED RESTORE POINTS ---
$report.AppendLine("`nORPHANED RESTORE POINTS") | Out-Null
$report.AppendLine("-" * 40) | Out-Null

$allBacked   = Get-VBRBackup | ForEach-Object {
    Get-VBRRestorePoint -Backup $_ | Select-Object -ExpandProperty VmName -Unique
} | Sort-Object -Unique

$orphaned = $allBacked | Where-Object { $_ -notin $protectedNames }
$report.AppendLine("  VMs with restore points but not in any job: $($orphaned.Count)") | Out-Null
if ($orphaned.Count -gt 0) {
    $orphaned | Sort-Object | ForEach-Object { $report.AppendLine("    $_") | Out-Null }
}

# --- OUTPUT ---
$report.AppendLine("`n" + "=" * 60) | Out-Null
$output = $report.ToString()
Write-Host $output

# Save to file (optional)
$logPath = "C:\VeeamReports\weekly-digest-$(Get-Date -Format 'yyyyMMdd').txt"
$logDir  = Split-Path $logPath
if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir | Out-Null }
$output | Out-File -FilePath $logPath -Encoding utf8
Write-Host "Report saved to: $logPath"

Disconnect-VBRServer

Key Takeaways

  • Repository sizing formula: (Full Backup + Incrementals * Retention) * 1.25 / Data Reduction Ratio. Use 2:1 as a conservative data reduction estimate. Use 3:1 for mixed workloads with good compressibility. Use 1:1 for encrypted or pre-compressed data.
  • Database servers (SQL OLTP) can change at 10 to 30% daily. Size database VM repositories separately from general workloads. Using 3% for everything in an environment with significant SQL produces dangerously undersized estimates.
  • ReFS (Windows) and XFS (Linux) with Fast Clone cut synthetic full space requirements dramatically. The 1.25x workspace multiplier drops to roughly 1.1x. For environments with weekly synthetic fulls, this is a meaningful capacity saving.
  • Forward incremental with synthetic fulls is the recommended chain type for almost all environments. Forever forward incremental works but creates merge I/O that hurts deduplication appliances. Reverse incremental is not recommended for new deployments.
  • SOBR capacity tier Move mode only works with forward incremental plus synthetic fulls. Forever forward incremental jobs can only use Copy mode. Pick your chain type before designing your SOBR offload strategy.
  • Watch the SOBR performance tier fill rate independently of total SOBR capacity. Jobs fail with space errors when the performance tier fills even if object storage has plenty of room. Keep the performance tier below 70%.
  • Enable "Remove deleted items data after" on every backup job. Without it, restore points for decommissioned VMs accumulate on the repository indefinitely. Never delete orphaned backup files manually from the filesystem.
  • Four Veeam ONE reports to run on a regular cadence: Protected VMs (weekly), Capacity Planning for Backup Repositories (monthly), Backup Job Statistics with a 30-day window (weekly), and Orphaned VMs (weekly).

Read more