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.
- Nightly integrity:
zstd -ton the new archive and a row count from the dump — under 10 seconds. - Weekly restore: unpack to a temp directory, run
mysqlcheckor start a throwaway container, then delete. Costs disk space for ten minutes. - 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.
