What a Proxmox Restore Actually Costs: Measured RTO

I restored real VMs and containers from Proxmox Backup Server and timed every step — restore speed, boot, and time-to-service — with one honest failure.

On this page
  1. What “restore cost” even means
  2. The restore rate that never moved
  3. Backup speed lies. Restore tells the truth.
  4. Time-to-restore is not time-to-service
  5. The restore that failed — and why it’s the best part
  6. What a verify job does and doesn’t promise
  7. Local is not offsite
  8. Measure your own — the whole point

Ask a homelab forum how long a Proxmox Backup Server restore takes and you’ll get a shrug wearing a number: “a few minutes, depends.” I’ve relied on that shrug for a year, which is a strange thing to do about the one operation you only run when something has already gone wrong. So I stopped guessing and measured it — properly, with a stopwatch on real guests, the same restore run three times, and every number written down next to the hardware and the network path that produced it.

This is what I found: a restore rate that never wavered, a boot that costs nearly as much as the restore, a backup speed that quietly lies to you, and one restore that failed outright and taught me the most. None of it is a spec sheet. All of it is measured.

First: make these values your own

Every number in this post is a measurement of my lab — local NVMe disks, a 1 gigabit network, and my particular guests. They are not properties of Proxmox and they will not be yours. The placeholders to change if you copy any command: the storage names local-lvm and pbs-local, the guest IDs 9001/9002, and any restore rate you see here. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy. The one honest way to get your numbers is to run the measurement yourself, which is exactly what the companion playbook is for.


What “restore cost” even means

The number that matters at 2am isn’t backup size or verify duration. It’s Recovery Time Objective — RTO, which NIST defines as “the overall length of time” a system can be down “before negatively impacting” what depends on it. In a homelab that translates to one blunt question: a guest just died — how many minutes until the thing it was running answers again?

That breaks into three measurable pieces, and most write-ups only ever quote the first:

  1. Restore — copying the guest’s disk back from the backup to a working node.
  2. Boot — the restored guest starting its kernel, systemd, and services.
  3. Service — the application inside actually listening and answering.

I timed all three. Let’s take them in order.


The restore rate that never moved

I built two disposable virtual machines to measure — the first VMs this otherwise all-container cluster has ever run. A small VM was a plain Debian 12 cloud image, roughly 1.2 GiB of real data on an otherwise empty disk. A large VM was that same image plus a 12 GiB second disk filled with /dev/urandom — deliberately incompressible random bytes, so nothing could be deduplicated or compressed away. That large VM is the honest worst case: every one of those gibibytes has to actually cross the wire.

Both were backed up to a local Proxmox Backup Server datastore on NVMe, then restored to local NVMe on another node over the 1 gigabit cluster LAN. I ran the large-VM restore three times.

Restore throughput vs. the 1 GbE ceiling12 GiB of incompressible data · PBS on NVMe → local NVMe · same LAN050100MB/s1 GbE line rate — 125 MB/sVirtual machineblock-level restore114 MB/sContainerfile-level restore (pxar)87 MB/sThe VM restore ran at 91% of wire speed — three runs, identical to the second. The disks were never the limit; the network was.

Here’s the table, with the hardware and mechanism stated inline so you can actually interpret the numbers rather than just be impressed by them:

Guest class Real data Wall clock Effective rate How it restores
Small VM ~1.2 GiB 9.7 s (10 / 9 / 10) ~1 GB/s* block-level
Large VM 12 GiB incompressible 120.3 s (121 / 121 / 119) 114 MB/s (σ ≈ 0) block-level
Small container 1.305 GiB 17 s 98.7 MiB/s (~103 MB/s) file-level (pxar)
Large container 13.407 GiB 169 s 83.2 MiB/s (~87 MB/s) file-level (pxar)
* Why the small VM looks impossibly fast

The small VM’s rate reads like a gigabyte a second, but nothing crossed the wire at that speed. Its disk is almost entirely empty, and block-level restore skips zero regions — so the ~10 seconds is mostly fixed overhead, with barely any real data to move. It’s a reminder to divide by real data, not provisioned size. The large VM, packed with incompressible random bytes, is the number to trust.

And it isn’t a reconstruction. Here’s a fourth restore of that large VM, captured live while writing this post — the PBS repository line redacted, everything else exactly as pbs-restore printed it:

A live restore of the large VM, captured for this post (PBS repository line redacted)

# base OS disk — mostly empty, so zero regions are skipped and it flies:
restore image complete (bytes=3221225472, duration=2.86s, speed=1073.12MB/s)

# the 12 GiB of incompressible data — the honest, wire-bound number:
progress  50% (read  6442450944 bytes, zeroes = 0% (0 bytes), duration  55 sec)
progress 100% (read 12884901888 bytes, zeroes = 2% (301989888 bytes), duration 107 sec)
restore image complete (bytes=12884901888, duration=107.36s, speed=114.46MB/s)

That 114.46 MB/s is a fourth run of the same test, landing right where the first three did. The wire-bound ceiling doesn’t move.

Three things fall out of that table. First, the large-VM rate was byte-identical across three runs — 114 MB/s, standard deviation effectively zero. That’s not a noisy sample I got lucky with; it’s a hard ceiling. And it’s exactly the ceiling you’d predict: 1 gigabit per second is 125 MB/s of raw line rate (a billion bits, divided by eight), and 114 is 91% of that, the rest lost to protocol overhead. The restore is network-bound, full stop. Faster disks wouldn’t move it a hair.

Second, block-level beats file-level for the same data. A VM restores by streaming disk blocks; a container restores by unpacking a pxar archive file by file, and per-file bookkeeping costs more than raw block streaming. 87 MB/s versus 114 for equivalent bytes.

Third — and this is the one that changes how you plan — restore time tracks a guest’s real, non-empty footprint, not the size you provisioned it. A 100 GB disk that’s 5 GB full restores like 5 GB, because the empty space was never in the backup to begin with.


Backup speed lies. Restore tells the truth.

While I had the stopwatch out, I timed the backups too, and they told a flattering story I’ve decided not to believe.

The small VM backed up in 8 seconds — 64% of it deduplicated, 62% of it zeroes that never got written. That’s Proxmox Backup Server’s content-defined chunking and deduplication doing exactly what it’s designed to do, and it makes the backup look blazing fast. But a backup that’s mostly “we already have these chunks, skip them” tells you nothing about how long the restore takes, because the restore still has to reassemble the whole guest.

The large VM, stuffed with incompressible random data that can’t dedup or compress, backed up at 105.9 MiB/s — the honest wire rate, with nowhere to hide. That’s the number worth knowing.

The rule I took away

Deduplication makes your backups fast and your storage cheap. It does nothing for your restore. When you’re estimating recovery time, measure a restore of real, incompressible data — never infer it from how quickly the backup finished.


Time-to-restore is not time-to-service

Here’s the gap nobody publishes. A restore that completes in 10 seconds has not given you a working service in 10 seconds. The guest still has to boot.

I measured this on the small VM using its own clock — systemd’s boot timestamps, read from inside the guest, so there’s no stopwatch lag in the number. The disk came back in about 10 seconds. Then the kernel and systemd took another 8 seconds to reach a running system with sshd actually listening on its port. Total time from “start the restore” to “the service answers”: about 18 seconds.

One restore, measured from the guest’s own clock0s10s20sRestore disk~10 sKernel + systemd~8 ssshd answers · t+18 sThe restore was 10 seconds. The service was down for 18. The boot is not free — measure it.

Eight seconds of boot doesn’t sound like much until you notice it’s nearly as long as the restore itself. On a bigger guest the restore dominates, but on small, fast-restoring services the boot is a real fraction of your downtime — and it’s the fraction every restore-time estimate silently drops. If you’re planning recovery, budget for it.

(One honesty note on my own method: a cloud-init apt step ran for another 37 seconds after sshd was already up. I excluded it, because the service was answering before it started — but if your first boot does real provisioning work, that time is yours to count.)


The restore that failed — and why it’s the best part

The most valuable paragraph in any measurement post is the one where something breaks, because that’s the part vendor marketing can’t reproduce. Mine broke on the containers.

I tried to restore a privileged container as unprivileged — flipping its security model on the way in, which felt like a clever shortcut. Proxmox’s own pct documentation had already warned me not to: for a restore, the unprivileged setting “defaults to the value from the backup” and, in their words, “should not be modified manually.” I modified it manually anyway. The restore died partway through unpacking:

What happens when you flip a container's privilege level on restore

extracting archive '.../root.pxar'
failed to set ownership: Invalid argument (os error 22)

The reason is how unprivileged containers work. An unprivileged container maps its internal root (UID 0) to a harmless high-numbered UID on the host — root inside is a nobody outside. Converting a privileged backup into that model means rewriting the ownership of every extracted file into the mapped range, and that remap is exactly where the pxar extraction hit an ownership call the kernel refused. Restoring the container with its original privilege level worked perfectly, in 169 seconds.

Preserve the privilege level on restore

A restore is the wrong moment to change a container’s privilege level. Restore it as it was backed up — the tooling defaults to that for a reason — and if you want to convert a privileged container to unprivileged, do it as a separate, deliberate operation when nothing is on fire. Discovering this rule during an actual outage is the expensive way to learn it.


What a verify job does and doesn’t promise

There’s one more number people quote as if it were a safety guarantee: the green checkmark on a Proxmox Backup Server verify job. It is worth having, and it is not what most people think it is.

A verify job re-reads your stored backup chunks and re-checks their CRC-32 checksums to catch corruption in the backup storage itself. The docs are refreshingly plain about why you’d re-run it: physical drives “are susceptible to damage over time, which can cause an old, working backup to become corrupted in a process known as bit rot.” So verification is real, and you should schedule it.

But read what it actually does: it checks that the bytes you stored are the bytes you stored. It never restores anything. It cannot tell you the guest will boot, that the filesystem inside is consistent, or that the database will come up clean. A backup can pass verification every night and still restore into a guest that panics on boot — verification would have no way to know.

'It verifies' is not 'it restores'

Chunk verification proves storage integrity, not recoverability. The only test that proves a backup restores is a restore. There’s a long-standing community feature request for PBS to run scheduled restore-and-boot tests (forum thread 161869); until that exists, restoring a real guest on a schedule — even occasionally, even by hand — is the only thing that earns you the confidence a green verify badge only implies.


Local is not offsite

Every number above is a local restore — same building, same 1 gigabit LAN, 114 MB/s. That’s the happy path, and it’s the one you’ll almost never be in when it matters, because the disaster that takes your node might take the room.

My offsite copy lives at a different location across an encrypted tunnel, and reading a restore back from it runs at about 3.1 MB/s — roughly 30 times slower than the local path. A restore that’s two minutes at home is an hour from away. That gap is the whole argument for keeping a fast local tier and an offsite one, and for knowing — measured, not assumed — how far apart they are.

There’s a deeper story in that offsite path, though. Writing backups out to that same location was even slower than reading them — mysteriously, stubbornly slower — and chasing down why turned into a proper detective story with a genuinely surprising culprit. That’s its own post: why a 208 KB kernel buffer throttled my offsite backups.


Measure your own — the whole point

I’m not going to tell you a Proxmox restore takes 114 MB/s, because on your hardware it doesn’t. The value of everything above isn’t the specific numbers — it’s the method, and the method is portable. Restore a real guest with real incompressible data, time it three times, watch the boot on the guest’s own clock, and try one thing that ought to fail so you learn where the edges are.

I wrapped exactly that into a script so you don’t have to reinvent it: it creates disposable test guests, fills one with incompressible data, backs them up, restores them while timing each run, and destroys everything it made. It defaults to a dry run that changes nothing, and it is emphatically not a product — no scheduling, no daemon, just a measurement aid you read before you run. It’s the Measure Proxmox Restore RTO playbook.

Run it once. Then you’ll have a real answer to the question you’ve been shrugging at — and a real number to plan the next disaster around, instead of “a few minutes, depends.”


Interactive: audit your 3-2-1 coverage

A measured restore only helps if the backup exists in the first place — check every data set you care about against the 3-2-1 Backup Planner and find the gaps before a restore day does.

Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.