Making Nightly Backups Cheap on a Small VPS: Scheduling, Compression and Retention Math

On a 1–2 GB VPS, a backup job is a competing workload: it wants CPU while your site is serving traffic, it wants disk bandwidth while MySQL is flushing, and it wants to write a file large enough to matter. Done carelessly, the nightly backup is the slowest thing on the box and the reason your monitoring alerts at 03:10 every night. Done properly, it is a 90-second job that barely registers. The difference is scheduling, priority and compression level — and those choices also decide how much storage you have to rent.

This is about the mechanics and cost of running the job itself. The rotation scheme is a separate decision; here the focus is how to make one nightly run cheap in CPU and I/O on hardware you are paying for by the gigabyte of RAM.

Cron or systemd timer: what each actually costs

Both wake a process once a night. The difference is bookkeeping, not CPU. Cron is a small daemon resident in memory (roughly 1–3 MB RSS), starts jobs with a minimal environment, and gives you nothing if the job fails silently. A systemd timer adds Persistent=true (run the missed job after downtime), RandomizedDelaySec (spread load across hosts), captured logs via journald, and cgroup accounting so you can see exactly what the backup cost.

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
Nice=15
IOSchedulingClass=idle
MemoryMax=300M

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 03:20:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target

On a 1 GB box the systemd unit’s MemoryMax=300M is the useful part: it caps the compression process so a runaway tar cannot push the site into swap. Cron can approximate this with ulimit -v in the script, but the accounting in systemctl status backup.service is worth the extra file.

nice, ionice and CPU cost

Scheduling priority is the cheapest optimisation available and the most often skipped.

  • nice -n 19 — the backup yields CPU to everything else. On a single vCPU this is the difference between a responsive site and a 3-second page load during the job.
  • ionice -c 3 — idle I/O class. Disk writes only happen when nothing else is waiting. On NVMe this matters less, on budget SATA-backed plans it matters a lot.
  • cpulimit --limit 40 — hard ceiling, useful when compression would otherwise eat both vCPUs.
nice -n 19 ionice -c 3 zstd -T1 -3 -q /var/lib/mysql/dump.sql \
  -o /srv/backup/db-$(date +%F).sql.zst

Measure the actual cost rather than assuming. /usr/bin/time -v reports wall time, user CPU and peak RSS for the whole job, and comparing that across a week tells you whether the job is CPU-bound or I/O-bound.

Compression level versus CPU: the real trade

zstd is the right default on a small box because the level is a dial, not a policy. The table below is typical for a 1 GB SQL dump and a mixed 2 GB file tree on one vCPU of a modern shared core.

Method / level CPU time (approx) Output size Notes
gzip -6 75 s 100% Baseline, single-threaded, slow
zstd -1 18 s 118% Fastest; good for daily fulls
zstd -3 32 s 100% Default; the usual sweet spot
zstd -9 140 s 86% Only for cold archives
zstd -19 900 s+ 78% Never on a small VPS, rent storage instead
XZ -6 400 s 80% No reason to choose over zstd -9

The arithmetic that matters: going from zstd -3 to zstd -9 saves 14% of the archive size for 4.4× the CPU. If your backups are 10 GB, that saves 1.4 GB of storage — about $0.03/month at object-storage rates — and costs two extra minutes of CPU on the box every night. Unless you are storing on the VPS’s own expensive disk, level 3 is the right answer, and -1 is right if the job ever overruns.

Also drop the pointless work: exclude cache directories, node_modules, session files and log rotations before compressing, not after. Filtering 30% of the bytes is free compared with compressing them with a heavier level.

Incremental instead of full: what it really buys

A full nightly dump of a 4 GB database costs 4 GB of writes every night, 120 GB a month, regardless of how little changed. On a small box that is often the largest recurring disk write you have, and it is pure overhead if your data changes 2% a day.

Strategy Monthly write volume Restore complexity Best fit on a small VPS
Nightly full 120 GB One file Under 2 GB of data
Weekly full + nightly differential ~40 GB Two files Most 1–2 GB boxes
rsync –link-dest snapshots Changed blocks only Pick a directory Filesystem data, not databases
Filesystem snapshots (LVM/ZFS) Metadata + delta Mount and copy Only if provider supports it

For databases the practical middle ground is a weekly full logical dump plus binary-log shipping or daily incremental dumps. For file trees, rsync --link-dest gives you daily directory-shaped snapshots at the cost of the changed files only — usually 50–200 MB a night instead of 4 GB.

rsync -a --delete --link-dest=/srv/backup/latest \
  /var/www/ /srv/backup/staging/ && mv /srv/backup/staging /srv/backup/$(date +%F)

Restore tests without doubling the work

An untested backup is a rumour. You do not need a full restore every night; you need a cheap check every night and a real restore on a schedule.

  1. Nightly integrity: zstd -t on the new archive and a row count from the dump — under 10 seconds.
  2. Weekly restore: unpack to a temp directory, run mysqlcheck or start a throwaway container, then delete. Costs disk space for ten minutes.
  3. Monthly full restore: restore to a second VPS or a local VM and confirm the site serves a page. This is the only test that proves the backup plus your runbook works.

Keep the restore commands in the same repo as the backup script. The night you need them is not the night to reconstruct them.

How the strategy changes the storage you rent

This is where the job design turns into a purchasing decision. Three shapes, priced at current small-VPS and object-storage rates:

Design Data held Extra monthly cost Trade-off
Local disk only, 7 dailies 4 GB × 7 = 28 GB $0 if plan has 60 GB Dies with the provider; fills the disk during an incident
Local + daily push to object storage 28 GB remote ~$0.65 + egress Survives provider loss; the default choice
Remote only, 30 dailies + 12 monthlies ~50 GB remote ~$1.15 Needs decent uplink; no local copy for fast restore

Note what the table implies: cheaper backups buy you a smaller disk. If you push everything off-box, a 40 GB plan is plenty and you can stay on the $5 tier; if you keep 90 days locally, you need 90 GB and the plan jumps a tier, which costs more than the storage you saved. Choosing zstd -1 plus 14-day retention can keep you on the smaller disk tier — that is a real $2–3/month, and it is the actual reason compression level matters on a budget box.

Putting the job together

A workable nightly script on a 2 GB box: systemd timer at 03:20 with a 15-minute random delay; nice 19, ionice -c 3; database full on Sunday, differential on other days; files via rsync --link-dest; zstd -3 for anything going off-box and -1 for local copies; retention handled by a find command with -mtime +14; a push to object storage with rclone; and a nightly zstd -t check that fails loudly. Total CPU cost: under 40 seconds a night and peak RSS under 250 MB if you stream instead of building tarballs in memory.

That profile is what lets you stay on a 2 GB, 40 GB plan rather than buying a 4 GB, 160 GB one purely for backup headroom. Compare the two tiers on our small VPS plan comparison before you size the backup strategy around a disk you did not need.

Affordable-Vps-Server-Author
Affordable-Vps-Server-Author
Articles: 393

Leave a Reply