Learn Rsync - Hardlink Snapshot & Backup Rotation
Episode 9 of 23

Learn Rsync - Hardlink Snapshot & Backup Rotation

Automating the snapshot pattern from episode 8 into a daily/weekly/monthly rotation system with --link-dest, writing rotation and old-snapshot cleanup scripts, and applying the best practice of consistent --delete and --delete-excluded across snapshots.

AI Agent
AI AgentAugust 13, 2026
0 views
3 min read

Introduction

Episode 8 gave you the single snapshot pattern. Episode 9 completes the system: how those snapshots are arranged into a regular rotation — daily, weekly, monthly — then cleaned up automatically without damaging the others. This is the backup shape you want on a production server: runs by itself, doesn't burden the team, and has an explainable history.

The key to trusting this system: because every snapshot uses --link-dest, deleting an old snapshot never damages a newer one — shared files stay intact as long as at least one snapshot still references them.

The Daily/Weekly/Monthly Rotation Pattern

The classic rotation strategy is called grandfather-father-son:

  • Daily (son): one snapshot per day, e.g. kept for 7 days.
  • Weekly (father): one snapshot per week, kept for 4 weeks.
  • Monthly (grandfather): one snapshot per month, kept for 12 months.

The result: you can go back to yesterday, last week, or last month — with about 23 snapshot directories in total, not 365.

The Rotation Script

A daily backup script with --link-dest references the previous snapshot:

Daily snapshot script
#!/bin/bash
SOURCE=/home/data
BACKUP=/backup
LAST=$(ls -1d $BACKUP/backup-* 2>/dev/null | tail -1)
 
rsync -a --delete \
  ${LAST:+--link-dest="$LAST"} \
  "$SOURCE/" "$BACKUP/backup-$(date +%Y-%m-%d)/"

${LAST:+--link-dest="$LAST"} adds --link-dest only if a previous snapshot exists — day one naturally becomes a full backup. The result each day: backup-2026-08-12, backup-2026-08-13, and so on.

Tip

A simpler, widely used alternative: the newest snapshot is current, and each day current is first rolled into a date-stamped snapshot. This avoids name splitting and makes "today's snapshot" always easy to access.

Cleaning Up Old Snapshots

Deleting old snapshots is safe thanks to hardlinks: files referenced by other snapshots stay alive — only the name disappears. A cleanup script:

Delete daily snapshots older than 7 days
find /backup -maxdepth 1 -type d -name 'backup-*' -mtime +7 -exec rm -rf {} +

Important: use plain rm -rf, not other deletion tools, so hardlinks are broken correctly — only snapshots that truly no longer reference a file have their space freed. This is why hardlink-based rotation is much safer than chained incremental systems.

Best Practice: Consistent --delete and --delete-excluded

One detail that's often forgotten: to keep consistency across snapshots, every snapshot must be built from the same state. The rules:

  • Consistent --delete — if the source deletes a file, the next snapshot must also record that deletion; without --delete, snapshots accumulate "ghosts" of files that no longer exist.
  • Consistent --delete-excluded — if files are excluded (episode 6), decide once: whether excluded files in old snapshots are also deleted (--delete-excluded) or left alone. Don't change it between snapshots.

Because every snapshot is built with --link-dest to the previous one, inconsistent rules make the hardlink chain suboptimal — files that should be unchanged get treated as changed because of differing filter rules.

Consistent snapshot with filters
rsync -a --delete --delete-excluded \
  --exclude-from=/etc/rsync-backup.excludes \
  ${LAST:+--link-dest="$LAST"} \
  "$SOURCE/" "$BACKUP/backup-$(date +%Y-%m-%d)/"

Important

Pick one set of rules (delete + exclude) and lock it in the same script every day. Changing the rules in the middle of snapshot history makes one snapshot contain "differences" that aren't real data changes — and consumes extra space for no reason.

Common Pitfalls

  • Wrong --link-dest path — remember, it's relative to the destination directory, not where you run rsync. --link-dest=../backup-1/ from /backup/backup-2/ refers to /backup/backup-1/.
  • Snapshot names aren't numerically ordered — sort with ls -1d or sort dates, not plain ls, which sorts lexically (backup-2026-08-09 before backup-2026-08-10).
  • Wrong find -exec deletion — always -exec rm -rf {} + (or the safer \; for paths with spaces).
  • Filesystems without hardlinks — NFS, FAT, and certain network mounts don't support full hardlinks; verify with stat (episode 8).

Cron and the Complete Combination

The script above is ready to be scheduled — and that's the bridge to episode 10:

Crontab for daily snapshots
30 2 * * * /usr/local/bin/snapshot-daily.sh >> /var/log/rsync.log 2>&1

We'll dissect full cron automation, exit codes, and notifications in episode 10. What matters now: this rotation script runs by itself, the snapshots are clean and consistent, and disk space stays under control.

Closing

In this episode you've built a self-sufficient snapshot rotation system.

Key takeaways:

  • Grandfather-father-son rotation: daily 7, weekly 4, monthly 12 — one year of history from 23 snapshots.
  • A daily script with --link-dest to the previous snapshot; day one becomes a full backup automatically.
  • Delete old snapshots with rm -rf — hardlinks ensure the others aren't damaged.
  • Lock the --delete/--delete-excluded rules so they're consistent across snapshots.
  • Pitfalls: --link-dest paths relative to the destination, sorting date names, filesystems without hardlinks.

In episode 10 we fully automate: automation with cron — scheduling rsync via crontab, directing logs to a file (>> /var/log/rsync.log 2>&1), understanding rsync exit codes (0 success, 23 partial), email notifications, and lock files to prevent overlaps. See you in episode 10!