Off-Site VPS Backups to S3: Why rclone Sends 50× More Than restic

  • Tested: Ubuntu 24.04 LTS
  • Restore verified by hash
  • 217 MB/night becomes 4.4 MB
  • 6 September 2026
  • 9 min read

Published 6 September 2026. Measured on one live server we operate — 2 vCPU, 7.8 GiB, Ubuntu 24.04 — which runs five production sites and had, until this article, every one of its backups sitting on the same disk as the data they protect. Transfers were made to a MinIO instance on the same machine, so the throughput figures measure disk and CPU rather than the internet. The volumes are what would cross the wire to a real provider, and those are the numbers that matter.

1. The Backups Were Already Good, and Worthless

Before adding anything, here is what the server already did every night, one cron entry per site between 03:20 and 03:45:

bash
20 3 * * * /usr/local/bin/car365-backup-db
30 3 * * * /usr/local/bin/capersmed-backup-db
35 3 * * * /usr/local/bin/vinaigredumaroc-backup-db
40 3 * * * /usr/local/bin/laboratoiretanger-backup-db
45 3 * * * /usr/local/bin/stackrecipes-backup-db

Each one dumps its database, tars the application directory, rotates at fourteen days and appends to a log. That is a good backup script. All of it landed in /var/backups, on the same filesystem as /var/www, on the same virtual disk, on the same host.

A backup on the machine it protects survives exactly one class of failure: you deleting a file. It does not survive a failed disk, a provider incident, a compromised root account, or an accidental rebuild of the instance. Those are the events people actually buy backups for.

The nightly production, measured across all five sites:

Measured
Total held locally1.9 GB across 1,631 files
New data produced each night217 MB
Local retention14 days

217 MB a night is the figure that decides the cost of everything that follows.

2. The Obvious Approach, and the Trap in It

The reflex is rclone sync to an S3-compatible bucket, and it works well. The first run pushed 1.6 GB and 1,626 files. A second run with nothing changed took 1.2 seconds and transferred zero files — the comparison is by size and modification time, so an unchanged archive costs a listing and nothing more.

bash
rclone sync /var/backups objstore:vps-backups

Then we deleted one backup file locally — the kind of thing a bug, a misfired find -delete, or ransomware does — and ran the same command again.

bash
Before: 237158  stackrecipes-20260906-1902.sql.gz   (present remotely)
Local:  rm stackrecipes-20260906-1902.sql.gz
After:  rclone sync ...
Result: the file is gone from the remote copy too
Warning

sync makes the destination match the source, deletions included. An off-site copy that faithfully mirrors your local damage is not a backup, it is a second copy of the problem — and it usually gets discovered at the moment it is needed.

The one-flag fix moves anything about to be deleted or overwritten into a dated directory instead:

bash
rclone sync /var/backups objstore:vps-backups \
  --backup-dir "objstore:vps-backups-attic/$(date +%Y%m%d-%H%M%S)"

Repeating the deletion with that flag, the file was preserved:

bash
237158  20260906-1903/stackrecipes-db/stackrecipes-20260906-1902.sql.gz

If your provider supports object versioning, enable that as well. Two independent protections against the same failure is not redundant here, because the whole point of an off-site copy is surviving something that went wrong on the machine issuing the commands.

3. The Same Job With Deduplication: 217 MB Becomes 4.4

Consecutive nightly database dumps are nearly identical. A mirroring tool cannot know that: yesterday’s dump and today’s are two different files, so both are uploaded in full. A content-addressed tool splits files into chunks and stores each distinct chunk once.

We ran both against the same starting point, then produced a real night of backups and re-ran each:

rclonerestic
First run, 1.6 GB source21.7 s21.5 s
Stored in the bucket1.625 GiB1.144 GiB (1.31× compression)
New backup data produced217 MB
Transferred that night217 MB4.369 MiB
Incremental run time2.3 s1.2 s

A factor of fifty on the nightly transfer. Extrapolated over a year, that is the difference between uploading 77.3 GB and uploading 1.6 GB for the same protection.

Measured

restic reported a 1.31× compression ratio on data that was already gzipped, which surprised us enough to check. The gain is not from recompressing the archives — it comes from the uncompressed material in the same tree, and from chunk-level deduplication across the fourteen daily copies of nearly identical dumps.

4. What Someone With Your Bucket Keys Can Read

These archives contain database dumps, and database dumps contain password hashes, customer records and often the application’s own credentials. Off-site means the data now sits somewhere you do not control, protected by an access key that lives in a config file on the server most likely to be compromised.

We fetched one object from each repository through the S3 API, exactly as someone holding those keys would:

bash
# From the rclone bucket
$ rclone cat objstore:vps-backups/stackrecipes-db/...sql.gz | gzip -dc | head -1
-- MySQL dump 10.13  Distrib 8.0.46, for Linux (x86_64)
# 13 readable INSERT statements followed.

# From the restic repository
$ rclone cat resticstore:vps-restic/data/01/01413e8e... | head -c 32 | xxd
00000000: e16d 8a68 f880 c18b 26c2 e438 5a4d c97a  .m.h....&..8ZM.z
# Nothing decompresses. Nothing is readable without the repository password.

restic encrypts every chunk before it leaves the machine; the bucket credentials grant access to ciphertext and nothing else. With a plain mirror, the bucket credentials are the data.

If you prefer rclone for its simplicity, it has an encrypted remote (crypt) that wraps the destination. Use one of the two. Uploading unencrypted database dumps to object storage is the part of this setup that turns a good night’s work into a breach notification.

5. A Backup You Have Not Restored Is Not a Backup

We restored a database dump out of the repository and compared it byte for byte with the original, because “the upload succeeded” is not evidence of anything:

sql
$ restic restore 498d1b9f --target /tmp/restore-test --include "$FILE"
Summary: Restored 4 / 1 files/dirs (231.600 KiB) in 0:00

source    : b928a18c4e7226e3013271aa75ad9d97
restored  : b928a18c4e7226e3013271aa75ad9d97
gzip -t   : archive valid
first line: -- MySQL dump 10.13  Distrib 8.0.46, for Linux (x86_64)
INSERT statements: 16

Identical hash, valid gzip, and a dump that parses. That took 0.8 seconds, and it is the only part of this whole exercise that proves anything.

Warning

our first attempt at this test looked like a pass and was not. The file glob matched nothing, both hashes were empty strings, and the comparison printed a cheerful “IDENTICAL” for two empty values. A verification script that succeeds on missing input is worse than no verification, because it produces confidence. Assert that the file exists and that the hash is non-empty before comparing.

The repository’s own integrity check is separate and worth scheduling weekly:

bash
$ restic check
[0:00] 100.00%  2 / 2 snapshots
no errors were found

6. The Script

This runs after the per-site backup scripts have finished. It assumes they have already produced their archives in /var/backups.

bash
#!/bin/bash
# /usr/local/bin/offsite-backup — nightly off-site copy with restic.
# Runs after the per-site backup scripts. chmod 700, root only.
set -euo pipefail

# Credentials live in a root-only file, not in this script and not in cron.
# /etc/offsite-backup.env, chmod 600:
#   export AWS_ACCESS_KEY_ID=...
#   export AWS_SECRET_ACCESS_KEY=...
#   export RESTIC_REPOSITORY=s3:https://ACCOUNT.r2.cloudflarestorage.com/BUCKET
#   export RESTIC_PASSWORD=...
. /etc/offsite-backup.env

LOG=/var/log/offsite-backup.log
log() { echo "$(date -Is) $*" >> "$LOG"; }

# One run at a time. A slow night must not overlap the next one.
exec 9>/var/lock/offsite-backup.lock
flock -n 9 || { log "SKIP another run holds the lock"; exit 0; }

log "START"

restic backup /var/backups \
    --tag nightly \
    --exclude '*.log' \
    --host "$(hostname)" >> "$LOG" 2>&1

# Retention on the REMOTE copy, independent of local rotation.
restic forget \
    --keep-daily 7 --keep-weekly 4 --keep-monthly 6 \
    --prune >> "$LOG" 2>&1

# Cheap structural check nightly; the full data read is expensive, so it
# gets its own weekly cron entry with --read-data-subset.
restic check >> "$LOG" 2>&1

log "OK $(restic stats --mode raw-data --json | head -c 200)"
bash
# Nightly, after the last per-site backup at 03:45
55 3 * * * /usr/local/bin/offsite-backup

# Weekly, read and verify a tenth of the actual data
30 4 * * 0 . /etc/offsite-backup.env && restic check --read-data-subset=10% \
           >> /var/log/offsite-backup.log 2>&1
Warning

restic forget --prune deletes snapshots on the remote. If the credentials in that file are stolen, they can be used to destroy the backups as well as read them. Where your provider offers it, use a second set of write-only or append-only credentials for the backup job and keep the pruning credentials somewhere else — the server holding the data should not be able to erase its own off-site copy.

7. Choosing the Destination

We did not open an account with any provider for this article, so we are not quoting prices we have not paid — check the current rates, they change. What does not change is the structure of the bill, and with the volumes measured here you can compute it yourself:

What you are billed forYour figure, measured
Stored bytes1.14 GB, growing ~4.4 MB per night before retention
Upload (usually free)4.4 MB per night, 1.6 GB per year
Download, on restore onlyUp to 1.14 GB, in the worst case
API operationsA few hundred per night with restic; 1,631 object listings per night with a mirror

The line that catches people is the third one. Egress is where object storage makes its money, and a full restore is the one moment you will be downloading everything — under pressure, with a site already down. Cloudflare R2 does not charge for egress and Backblaze B2 includes an allowance tied to stored volume; both are structural properties worth verifying before you commit, because a cheap-per-gigabyte provider with expensive egress prices your worst day.

At these volumes the whole thing costs cents a month with any of them. The decision is not price, it is what a restore costs you.

8. Frequently Asked Questions

Should I use rclone or restic for VPS backups?

restic, if the source is nightly archives. On our server 217 MB of new backups deduplicated to 4.4 MB, a factor of fifty, and restic encrypts before upload. Use rclone when you need a browsable mirror of files someone else must read without special tooling, and add its crypt remote if the data is sensitive.

Does rclone sync delete my remote backups?

Yes. Sync makes the destination match the source, so a file deleted locally is deleted remotely on the next run. We reproduced this. Add --backup-dir pointing at a dated path so removals are moved rather than destroyed, and enable object versioning at the provider as a second layer.

How much bandwidth do nightly off-site backups use?

It depends entirely on the tool. Our five sites produce 217 MB of new archives each night. A mirroring tool uploads all of it, 77 GB over a year. A deduplicating tool uploaded 4.4 MB, or 1.6 GB a year, for the same protection and the same restore capability.

Are my backups encrypted on Cloudflare R2 or Backblaze B2?

Not by you, unless you encrypt before uploading. With a plain mirror, anyone holding the bucket keys can download a dump and read it — we did exactly that and got a readable MySQL dump. restic encrypts client-side, so the same keys return unusable ciphertext without the repository password.

How often should I test restoring a backup?

Every time the backup configuration changes, and on a schedule otherwise. Restoring one file and comparing its SHA-256 with the original took under a second here. Make the check assert that the file exists and the hash is non-empty — ours initially compared two empty strings and reported success.

9. The Short Version

  • Backups on the machine they protect survive only one failure mode: your own rm.
  • rclone sync propagates deletions. Use --backup-dir, and versioning at the provider.
  • 217 MB of nightly archives deduplicated to 4.4 MB — 77 GB a year against 1.6 GB.
  • Encrypt before uploading, or your bucket keys are your database.
  • Restore something and compare hashes, and make sure the test cannot pass on missing files.

For what these backups are protecting, our capacity model describes the five-site setup on this server, and the hardening checklist covers the perimeter that keeps a compromise from reaching them in the first place.