Debian

Borg vs Restic vs Kopia: Backup Tools Benchmarked

If you’re trying to choose between Borg vs Restic vs Kopia, don’t ask which one is fastest, because each tool wins a different column. On the same dataset of about 5.5 GiB, Restic built the smallest repository and ran the quickest full data check. Borg used the least memory during the first backup and the full restore, restored everything in about 61 seconds, while Kopia with its defaults wrote twice the repository and took 899 seconds to restore it.

Original content from computingforgeeks.com - post 165101

Every number here was measured again for this update, three runs per test with the page cache dropped first, and the tables show the median. You will install all three tools on Debian 13, build the same test data, then back up, restore, verify and prune with each one, so you can repeat any result on your own disk.

Verified on Debian 13 in October 2026 with BorgBackup 1.4.5, Restic 0.19.1 and Kopia 0.23.1, and the numbers are from that run.

Compare the Borg, Restic and Kopia benchmark results

You can see the whole benchmark in one table. Times are in seconds and memory is peak resident memory from /usr/bin/time -v. Kopia appears twice because it doesn’t compress anything until you set a compression policy, and that one setting changes its repository size more than any other choice on this page.

Median of 3 runsBorgResticKopia (default)Kopia (zstd-fastest)
First backup (s)96103154115
Second backup, nothing changed (s)296.05.75.7
Backup after the known change (s)2715.619.515.8
Full restore (s)6152899559
Restore one 2 MiB file (s)2.42.54.06.2
Default check (s)11.42.34.02.7
Full data check (s)3317.910149
Repository after first backup (GB)2.131.853.851.88
Repository growth after the change (MB, median of per-run growth)175181217182
Peak memory, first backup (MiB)119151341272
Peak memory, full restore (MiB)122200390416

Restic is the best all-rounder on these numbers. It built the smallest repository, ran the quickest checks and finished an incremental backup in 6.0 seconds when nothing had changed. Borg kept up on the first backup and the full restore and used the least memory in those two steps (119 and 122 MiB at peak), but it spent 29 seconds on a run with nothing to do. About 12 of those seconds come from a deliberate Borg rule, covered in the backup section, that re-reads the newest file in the set. Once a small file took that place, Borg’s no-change run dropped to 16.8 seconds, which is still nearly three times Restic’s.

Kopia’s results split in two. Its no-change run was as quick as Restic’s, and you’ll only wait a few seconds longer after a change. A full restore is another story: it took 899 seconds with default settings, about 15 times longer than Borg or Restic on the same disk. A cold re-run later the same day took 292 seconds, about 5 times longer, so the size of that gap depends heavily on the host’s disk I/O. Kopia also used the most memory in every step. Turning on compression is the one change you should make on day one: with zstd-fastest the repository dropped from 3.85 GB to 1.88 GB, level with Restic, and the full restore came down to 559 seconds.

Check how the test was run

A benchmark is only worth something if you can see what produced it. Our VM ran on a Proxmox host with an Intel Core i7-6700 (AES-NI, no SHA extensions) that it shared with other running guests. That is why every test ran three times and the table reports the median, not the best run. You can check each of these against your own setup:

  • The VM had 2 vCPUs, 4 GB of RAM and two virtio disks backed by ZFS on the host.
  • The dataset and every restore sat on the 40 GB system disk.
  • Every repository lived on a separate 30 GB ext4 disk mounted at /srv/backup.
  • The dataset held 86,919 files and about 5.5 GiB, built by the script in the dataset section.
  • Each cycle rebuilt the dataset and started from an empty repository with no tool caches.
  • Every timed step started after sync and a dropped page cache, so nothing was served from RAM.
  • The runs were interleaved tool by tool, so a noisy minute on the host hit every tool and not just one.

Every tool used its own defaults apart from the encryption mode Borg makes you pick and the compression policy in the second Kopia column. The spread between runs was real. Kopia’s full restore ranged from 546 to 938 seconds and Restic’s first backup from 95 to 210 seconds. So when you compare tools, read a gap of a few seconds as a tie and trust the differences that show up as multiples.

Install Borg, Restic and Kopia on Debian 13

Debian 13 ships older builds of Borg and Restic and does not package Kopia at all, so we installed the current upstream release of each tool. That is what the numbers above describe. Before you start, make sure you have these:

  • You have a Debian 13 machine and a user with sudo rights.
  • You have a second disk or a USB drive for the repositories.
  • You have about 30 GB free on the system disk if you want to build the test dataset, keep a full restore from all three tools and leave room for Kopia’s local cache (up to about 5 GB by default), or less if you delete each restore after checking it.

On a fresh server, go through the Debian 13 post-install checklist first. Then install the helper packages, which the dataset scripts and the timing commands rely on:

sudo apt update
sudo apt install -y curl ca-certificates gnupg bzip2 xz-utils time qemu-utils openssl fuse3 python3 vim

The fuse3 package is only there so you can mount archives later, and vim is for the two scripts you’ll create, in case your image ships without it. Nothing in the benchmark needs FUSE.

Install Borg from the upstream binary

Borg’s GitHub releases ship a single-file binary and a .tgz that unpacks into a directory. The README attached to each release says the directory build starts faster and needs less space in /tmp, so use that one. The detection below only matches 1.4.x tags, so a Borg 2.0 release or a 1.2.x maintenance release can’t slip in and break the 1.4 commands this guide uses:

BORG_VER=$(curl -s https://api.github.com/repos/borgbackup/borg/releases | grep -o '"tag_name": "1\.4\.[0-9]*"' | head -1 | cut -d '"' -f 4)
echo "$BORG_VER"
curl -sLO "https://github.com/borgbackup/borg/releases/download/${BORG_VER}/borg-linux-glibc235-x86_64-gh.tgz"
sudo tar -xzf borg-linux-glibc235-x86_64-gh.tgz -C /opt
sudo ln -sf /opt/borg-dir/borg.exe /usr/local/bin/borg
borg --version

You should see the detected tag and then the version Borg reports:

1.4.5
borg 1.4.5

If you’d rather stay on Debian packages, sudo apt install borgbackup gives you the older 1.4 build from the Debian archive, with the same repository format. We did not benchmark that build, so your numbers may differ slightly from the table.

Install Restic from GitHub

Restic ships one static Go binary per platform, compressed with bzip2, so you only need to unpack it. The tag carries a leading v that the file name drops, which is what ${RESTIC_VER#v} handles:

RESTIC_VER=$(curl -s https://api.github.com/repos/restic/restic/releases/latest | grep tag_name | cut -d '"' -f 4)
echo "$RESTIC_VER"
curl -sLO "https://github.com/restic/restic/releases/download/${RESTIC_VER}/restic_${RESTIC_VER#v}_linux_amd64.bz2"
bunzip2 "restic_${RESTIC_VER#v}_linux_amd64.bz2"
sudo install -m 755 "restic_${RESTIC_VER#v}_linux_amd64" /usr/local/bin/restic
restic version

The last line confirms the binary and the Go version it was built with:

restic 0.19.1 compiled with go1.26.4 on linux/amd64

Debian’s own restic package is an older release, and the upstream binary doesn’t depend on anything else on the system, so it’s safe to replace later with a newer download.

Install Kopia from its APT repository

Kopia publishes its own signed APT repository, which is the easiest way to keep it updated with the rest of the system. These are the commands from the Kopia installation docs, plus the keyring directory that a minimal Debian image may not have yet:

sudo install -m 0755 -d /etc/apt/keyrings
curl -s https://kopia.io/signing-key | sudo gpg --dearmor -o /etc/apt/keyrings/kopia-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kopia-keyring.gpg] http://packages.kopia.io/apt/ stable main" | sudo tee /etc/apt/sources.list.d/kopia.list
sudo apt update
sudo apt install -y kopia
kopia --version

Kopia prints its version with the commit it was built from:

0.23.1 build: 72ec08fd8edb86c67ed27099bf1b955e1f308ffa from: kopia/kopia

All three tools are now on your PATH. If one of the version commands says “command not found”, open a new shell or check that /usr/local/bin is in your $PATH before you move on.

Borg vs Restic vs Kopia versions and a no-change Restic backup on Debian 13

The screenshot also shows where this guide is heading: a second Restic backup of the full dataset that found nothing new and finished in six seconds.

Build the test dataset

You can skip this section if you only want to back up your own files. If you’d like to reproduce the numbers, you need the same data, and these two scripts rebuild it identically every time. You get the Linux 6.12 source tree (1.4 GiB of compressible text), 300 “photos” of 2 to 6 MiB (1.2 GiB) and a Debian 13 cloud image converted to a sparse 3 GiB raw disk. The “photos” are AES keystreams generated from a fixed passphrase, so they are incompressible like real JPEGs but byte-for-byte identical on every run. Create a working directory and the first script:

mkdir -p ~/bench
vim ~/bench/make-dataset.sh

Paste the script. The kernel version is pinned on purpose, because a moving dataset would make the results impossible to compare:

#!/bin/bash
# Rebuilds the same benchmark dataset every time: a kernel source tree,
# 300 incompressible "photos" and a sparse VM disk image.
set -euo pipefail
DATA="$HOME/bench/data"
CACHE="$HOME/bench/cache"
KERNEL="linux-6.12"   # pin: fixed benchmark dataset

mkdir -p "$CACHE"
cd "$CACHE"
[ -f "$KERNEL.tar.xz" ] || curl -sLO "https://cdn.kernel.org/pub/linux/kernel/v6.x/$KERNEL.tar.xz"
[ -f debian-13-genericcloud-amd64.qcow2 ] || curl -sLO https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2

rm -rf "$DATA"
mkdir -p "$DATA/photos"
tar -xJf "$KERNEL.tar.xz" -C "$DATA"

for i in $(seq -w 1 300); do
  size=$(( (10#$i % 5 + 2) * 1048576 ))
  head -c "$size" /dev/zero | openssl enc -aes-128-ctr -nosalt -pbkdf2 -pass "pass:photo$i" > "$DATA/photos/IMG_$i.jpg"
done

qemu-img convert -O raw debian-13-genericcloud-amd64.qcow2 "$DATA/vm-disk.raw"
sync
du -sh --apparent-size "$DATA"/*
find "$DATA" -type f | wc -l

The second script applies the “known change” used for the incremental test. It edits 200 C files, adds 25 new photos (100 MiB) and rewrites 64 MiB in the middle of the disk image, which is roughly what a day on a small server or a VM host looks like. Open it in your editor:

vim ~/bench/apply-change.sh

Add the following:

#!/bin/bash
# The "known change" between two backups: 200 edited source files,
# 25 new photos (100 MiB) and 64 MiB rewritten inside the VM image.
set -euo pipefail
DATA="$HOME/bench/data"

mapfile -t files < <(find "$DATA/linux-6.12/drivers/net" -name '*.c' | sort)
for f in "${files[@]:0:200}"; do
  echo "/* changed */" >> "$f"
done

for i in $(seq 301 325); do
  head -c 4194304 /dev/zero | openssl enc -aes-128-ctr -nosalt -pbkdf2 -pass "pass:photo$i" > "$DATA/photos/IMG_$i.jpg"
done

head -c 67108864 /dev/zero | openssl enc -aes-128-ctr -nosalt -pbkdf2 -pass pass:vmchange \
  | dd of="$DATA/vm-disk.raw" bs=1M iflag=fullblock seek=1024 conv=notrunc status=none
sync

Make both executable, then build your copy of the dataset. The downloads are about 700 MB, and the build took three minutes on our VM.

chmod +x ~/bench/make-dataset.sh ~/bench/apply-change.sh
~/bench/make-dataset.sh

The script ends by printing the size of each part and the file count. If you see these numbers, your dataset matches ours:

1.4G	/home/lorna/bench/data/linux-6.12
1.2G	/home/lorna/bench/data/photos
3.0G	/home/lorna/bench/data/vm-disk.raw
86919

The part that trips people up is the disk image. It’s 3 GiB on paper but only about 907 MiB is allocated, because qemu-img writes it sparse, and all three tools read the holes as zeros and deduplicate them. Do not run apply-change.sh yet: it belongs between the second and third backup.

Create an encrypted repository with each tool

A repository is the directory each tool writes its encrypted chunks into, and it should live on a different disk from the data it protects. We used a second virtual disk; on a laptop or a small server that’s usually a USB drive, and our roundup of the best external drives for Linux backups covers which ones hold up. Find the disk first, and take your time over it, because the next step erases whatever is on it:

lsblk -o NAME,SIZE,LABEL,MOUNTPOINT

The empty disk is the one with no partitions, no label and no mount point. Ours was /dev/sdb. Set yours in a variable and format it with a label. This wipes the disk, so check the name against the size column once more before you press Enter:

export BACKUP_DISK=/dev/sdb
sudo mkfs.ext4 -L backup "$BACKUP_DISK"

Mount it by label rather than by device name, then give your user ownership. The label matters: when we added another disk to the VM later, our backup disk moved from sdb to sdc, and the fstab entry still found it. The nofail option lets the machine boot normally when a USB backup drive isn’t plugged in, instead of stopping at an emergency shell. Run the chown after the mount, because the mounted filesystem’s root is owned by root and the tools would fail with “permission denied”:

sudo mkdir -p /srv/backup
echo "LABEL=backup /srv/backup ext4 defaults,nofail 0 2" | sudo tee -a /etc/fstab
sudo systemctl daemon-reload
sudo mount /srv/backup
sudo chown "$USER": /srv/backup
df -h /srv/backup

The last line should show the new disk mounted at /srv/backup. This is our 30 GB repository disk:

Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb         30G  2.1M   28G   1% /srv/backup

All three tools read their password from an environment variable, which is how the benchmark ran them without prompts. Set them once per shell, together with the path you’re backing up, and pick a real password for anything you keep:

export DATA="$HOME/bench/data"
export BORG_PASSPHRASE="BenchPass2026"
export RESTIC_PASSWORD="BenchPass2026"
export KOPIA_PASSWORD="BenchPass2026"

These variables vanish when you log out, so export them again in every new session. Now create the three repositories:

borg init --encryption repokey /srv/backup/borg
restic -r /srv/backup/restic init
kopia repository create filesystem --path /srv/backup/kopia

Kopia is the chattiest of the three, and you’ll want to read its output once. Its output starts with the algorithms it picked, then prints its default policy, which ends with the line Compression disabled. (excerpt):

Initializing repository with:
  block hash:          BLAKE2B-256-128
  encryption:          AES256-GCM-HMAC-SHA256
  key derivation:      scrypt-65536-8-1
  splitter:            DYNAMIC-4M-BUZHASH
Connected to repository.

Borg is the only one that makes you choose an encryption mode, and repokey is what its own help text suggests as the rule of thumb. The same text says repokey-blake2 is likely faster on a CPU without SHA extensions, which includes our i7-6700, but we kept the rule-of-thumb mode and didn’t time the other one. It stores the key inside the repository, so Borg prints a reminder to export it. Do that now, because a damaged repository with no key copy can’t be decrypted, even with the right passphrase:

borg key export /srv/backup/borg ~/borg-key-backup

Store that file somewhere other than the backup disk. Restic and Kopia keep their keys in the repository too and protect them with your password alone, so for those two the password is the thing you must not lose. This is what each tool chose for you, read back from the repositories we created:

DefaultBorg 1.4ResticKopia
EncryptionAES-CTR-256 with HMAC-SHA256 (repokey)AES-256-CTR with Poly1305-AESAES256-GCM-HMAC-SHA256
Compressionlz4auto (zstd), repository format 2none
Chunkingcontent-defined (buzhash)content-defined (Rabin)content-defined (DYNAMIC-4M-BUZHASH)
Retention presetnone, you run borg prunenone, you run restic forget10 latest, 48 hourly, 7 daily, 4 weekly, 24 monthly, 3 annual

Two rows matter more than you’d think, so look at them first. Kopia’s “Compression disabled” line is printed right after the repository is created, and it’s the reason for its large repository in the results table. Kopia also comes with a retention policy already defined, while Borg and Restic keep every snapshot until you tell them otherwise. If you’re setting Kopia up for your own data rather than reproducing our default column, run the compression command from the Kopia compression section below now, before your first snapshot.

Run the first backup

The first backup is the expensive one, because every byte gets read, chunked, compressed and encrypted. Run one per tool, and expect about two minutes each on a dataset this size.

borg create --stats /srv/backup/borg::run-1 "$DATA"
restic -r /srv/backup/restic backup "$DATA"
kopia snapshot create "$DATA"

Borg’s summary tells you the most of the three. This excerpt is from our first run, and the three size columns show the source tree being compressed and the image holes being deduplicated:

                       Original size      Compressed size    Deduplicated size
This archive:                5.96 GB              2.15 GB              2.11 GB

Restic reports the same idea in its own units: 3.5 GiB of unique data went in and 1.7 GiB was stored after zstd:

Files:       86919 new,     0 changed,     0 unmodified
Dirs:         5763 new,     0 changed,     0 unmodified
Added to the repository: 3.510 GiB (1.725 GiB stored)

processed 86919 files, 5.547 GiB in 1:34
snapshot 98072438 saved

Kopia prints one line for the snapshot, so you’ll read less. On the very first snapshot of a repository it also runs a full maintenance pass straight after, so don’t be surprised by the garbage-collection lines that follow it (excerpt):

Created snapshot with root k3d231c1daec880198b72347ab31c38eb and ID 2ff0649aedb76257ee92d02881aac07a in 1m42s
Running full maintenance...
GC found 93038 in-use contents (3.8 GB)

Borg and Restic finished their first backups within about 10% of each other, at 96 and 103 seconds. Kopia took 154 seconds by default and 115 with compression, although default Kopia was the faster of the two in our first run, so we can’t say whether compression or disk writes decided that gap. Don’t pick a tool on this row alone, though. It’s a one-time cost, the runs on our shared host spread from 106 to 282 seconds for Kopia, and the daily backups below matter far more.

Back up again after a change

The backups you run every night are incremental, so these are the timings that decide how long your backup window is. You’ll run them far more often than anything else here. First run the same three commands again with nothing changed, giving Borg a new archive name:

borg create --stats /srv/backup/borg::run-2 "$DATA"
restic -r /srv/backup/restic backup "$DATA"
kopia snapshot create "$DATA"

Restic and Kopia finished that run in about 6.0 and 5.7 seconds. Restic’s summary confirms that it trusted the parent snapshot and read nothing:

using parent snapshot 98072438

Files:           0 new,     0 changed, 86919 unmodified
Dirs:            0 new,     0 changed,  5763 unmodified
Added to the repository: 0 B   (0 B   stored)

Borg is the odd one out at 29 seconds, even though it stored 0 bytes. We looked into that afterwards on separate test repositories, so read the rest of this part as a diagnosis, not as steps for you to run here. Extra borg create runs at this point would add archives and change the check and prune output further down. Adding --list --filter=AME to borg create prints only the files Borg added or found modified, and on two more no-change runs it printed the same single line:

A /home/lorna/bench/data/vm-disk.raw

That has nothing to do with the file being sparse or a disk image. Borg’s files cache deliberately leaves out any file whose timestamp matches the newest one in the archive, which protects against a file changing right after a snapshot, as the Borg FAQ on “A” status for unchanged files explains. Our dataset script writes vm-disk.raw last, so it was always the newest file, and Borg read all 3 GiB of it on every run before finding the chunks already stored. The FAQ’s fix is to touch a small file so it becomes the newest one. On one of those test repositories we ran touch "$DATA/linux-6.12/README" and three more no-change backups. The first still read the image once, since it wasn’t in the files cache yet, and took 24.9 seconds. The next two listed only the small file:

A /home/lorna/bench/data/linux-6.12/README

Those two runs finished in 16.8 seconds each, against 28.5 seconds for the same no-change backup right before the touch (single runs). So the 29-second median in the results table is partly an artifact of how we built the dataset, and you won’t see all of it on ordinary data. In your own backups, Borg will re-read whatever file changed last, so if that’s a large image or database dump, have your backup script touch a small file in the backup set just before it runs borg create.

Now apply the known change and back up a third time:

~/bench/apply-change.sh
borg create --stats /srv/backup/borg::run-3 "$DATA"
restic -r /srv/backup/restic backup "$DATA"
kopia snapshot create "$DATA"

Restic’s counts are a good way for you to check that the change landed: 25 new photos, 200 edited C files plus the disk image, and only the changed chunks stored (excerpt):

Files:          25 new,   201 changed, 86718 unmodified
Dirs:            0 new,    42 changed,  5721 unmodified
Added to the repository: 202.804 MiB (172.516 MiB stored)

After the change, Restic needed 15.6 seconds, Kopia 19.5 (or 15.8 with compression) and Borg 27. The repositories grew by 175 MB for Borg, 181 MB for Restic and 182 MB for compressed Kopia, which is the 164 MiB of new random data plus a little metadata, so all three deduplicated the rewritten image correctly. Default Kopia grew by 217 MB.

Turn on compression in Kopia

Kopia’s default policy stores everything uncompressed, which is why its first repository came out at 3.85 GB against 1.85 GB for Restic. The policy only applies to data Kopia writes after you set it. In our replay of these steps we set it after three snapshots, and the repository stayed at 4.07 GB, exactly the size of the uncompressed benchmark repositories. So set it before the first snapshot, or start a fresh repository for compressed backups:

kopia policy set --global --compression=zstd-fastest

Kopia confirms the setting and runs a maintenance pass (excerpt):

Setting policy for (global)
 - setting compression algorithm to zstd-fastest
Running full maintenance...

In our second Kopia column the policy was set straight after repository create, before the first snapshot. The repository came out at 1.88 GB, about 1.5% larger than Restic’s, and the first backup took 115 seconds instead of 154. The full data check dropped from 101 to 49 seconds, because there was half as much to read. Nothing in our numbers argues for leaving it off.

Restore everything, then one file

A restore is the step you’ll run under pressure, so it’s worth knowing how long yours takes before you need it. Borg extracts into the current directory, Restic takes a --target, and Kopia restores a snapshot’s root object, which you can read from its snapshot list:

mkdir -p ~/restore/borg ~/restore/restic ~/restore/kopia
cd ~/restore/borg && borg extract /srv/backup/borg::run-3 && cd ~
restic -r /srv/backup/restic restore latest --target ~/restore/restic
ROOT=$(kopia snapshot list "$DATA" --json | python3 -c 'import json,sys; print(json.load(sys.stdin)[-1]["rootEntry"]["obj"])')
kopia restore "$ROOT" ~/restore/kopia

You won’t see any output from Borg when the extract succeeds. Restic and Kopia finish with a count you can check against the source, which held 86,944 files after the change. These lines are from our replay of the article’s commands on the same VM, an untimed run on a busy host, so the 2:52 in Restic’s summary isn’t the benchmark figure:

Summary: Restored 92769 files/dirs (5.645 GiB) in 2:52
Restoring to local filesystem (/home/lorna/restore/kopia) with parallelism=8...
Restored 86944 files, 5760 directories and 62 symbolic links (6.1 GB).

Borg and Restic both restored the full set in about a minute (61 and 52 seconds). Kopia needed 899 seconds by default and 559 with compression, and its three runs ranged from 546 to 938 seconds. Most of the gap in our re-runs comes from filling Kopia’s local cache and reading the repository to do it. When you restore, Kopia pulls every piece of content through ~/.cache/kopia, which held 87,147 content files and 4 GB afterwards. The output of kopia cache info shows a 5.2 GB soft limit for contents and a separate 5.2 GB limit for metadata. Kopia also fsyncs each cache file it writes. In one restore we traced with strace, there were 87,173 fsync calls: every one was on a cache file and none on the restored files. On our VM that cache sat on the same disk as the restore target.

Single re-runs later the same day back this up. With the cache cleared first, a full restore took 292 seconds; run again with the cache still warm, it took 105 seconds. A warm run skips both filling the cache and reading those chunks from the repository, and we didn’t time the fsync calls themselves.

You might expect a separate disk for the cache to help, so we also moved it to the repository disk with kopia cache set --cache-directory. That run took 364 seconds, so it didn’t help here, where both virtual disks share one ZFS pool on the host. If you pick Kopia, plan for its cache to grow to about 5 GB by default, a little more during a long restore, on top of the restored data. Expect any restore whose data isn’t already cached to be slow, and that includes a disaster recovery onto a new machine. Time a full restore on your own hardware before you count on it for a fast recovery.

Pulling back a single file is what most of your restores will really be. Borg takes the path without its leading slash, Restic uses --include, and Kopia appends the path to the root object. Give each one an empty target, because Restic skips a file that’s already identical in the target directory, and our first attempt into the full-restore folder reported “skipped 1 files/dirs”:

mkdir -p ~/restore/one/borg ~/restore/one/restic ~/restore/one/kopia
cd ~/restore/one/borg && borg extract /srv/backup/borg::run-3 "${DATA#/}/photos/IMG_150.jpg" && cd ~
restic -r /srv/backup/restic restore latest --target ~/restore/one/restic --include "$DATA/photos/IMG_150.jpg"
kopia restore "$ROOT/photos/IMG_150.jpg" ~/restore/one/kopia/IMG_150.jpg

The medians for that 2 MiB file were 2.4 seconds for Borg, 2.5 for Restic and 4.0 for Kopia, so single-file restores aren’t a reason to pick one tool over another. One more thing to know: none of the three kept the disk image sparse on restore, so 6.06 GB of files took 6.29 GB on disk. Borg and Restic have a --sparse flag and Kopia has --write-sparse-files for that, but we didn’t time them.

Verify the repository

A backup you never check is a guess, so make checks part of your routine. Each tool has a quick check that looks at the repository structure and a slower one that reads and decrypts the data, and you’ll want the slow one on a schedule, say once a month:

borg check /srv/backup/borg
borg check --verify-data /srv/backup/borg
restic -r /srv/backup/restic check
restic -r /srv/backup/restic check --read-data
kopia snapshot verify
kopia snapshot verify --verify-files-percent=100

Borg’s check prints nothing when the repository is healthy and exits with status 0. If you want to see it work, add -v, which reports each stage (excerpt from our replay):

Starting repository check
finished segment check at segment 17
Starting repository index check
Index object count match.
Finished full repository check, no problems found.
Starting archive consistency check...
Analyzing archive run-1 (1/3)
Analyzing archive run-2 (2/3)
Analyzing archive run-3 (3/3)
Archive consistency check complete, no problems found.

You know a check passed when you see the following:

  • Borg exits with status 0, and with -v it ends on “no problems found”.
  • Restic ends both checks with “no errors were found”.
  • Kopia ends with a “Finished processing” line, and the full check reports a non-zero “Read … files” count.

The default checks don’t do the same amount of work, so you can’t compare them directly. Borg’s plain check already reads every segment and verifies its CRC, which is why it took 11.4 seconds, while Restic’s 2.3 seconds and Kopia’s 4.0 seconds only cover the structure and metadata. For the full read, Restic was quickest at 17.9 seconds, Borg took 33, and Kopia took 101 seconds uncompressed or 49 with zstd. If you only run one of these on a schedule, make it the full read, because that’s the one that decrypts your data and proves you can get it back.

Set a retention policy

Without a retention policy a repository only grows, so set one before the disk fills. The policy below keeps 7 daily, 4 weekly and 6 monthly snapshots in all three tools. Borg and Restic apply it when you run the command, ideally right after each nightly backup:

borg prune --list --keep-daily 7 --keep-weekly 4 --keep-monthly 6 /srv/backup/borg
borg compact /srv/backup/borg
restic -r /srv/backup/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Our three archives were all made on the same day, which shows what “daily” means: one snapshot per calendar day. Borg kept the newest and the oldest archive and pruned the one in between:

Keeping archive (rule: daily #1):            run-3                                Sun, 2026-10-18 16:19:16 [01df919c4f55e9b0a3e8f6d8b6fc1afc1cf6b192fa96fbe25236b152efa37f72]
Pruning archive (1/1):                       run-2                                Sun, 2026-10-18 16:18:30 [e1416c7d8190f104a2da0b536f99d3c810ec7e5629cf28e542e868f181e41497]
Keeping archive (rule: daily[oldest] #2):    run-1                                Sun, 2026-10-18 16:08:09 [d9969b1042c6549a945e103e59d7efec9f5aa49277d91e99f172103c45fe1fd4]

After borg compact, borg list shows the two archives that are left:

borg prune keeping daily archives and borg list output on Debian 13

Restic made the same decision and showed you its reasons for each snapshot it kept:

Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
keep 2 snapshots:
ID        Time                 Host          Tags        Reasons                  Paths                   Size
-------------------------------------------------------------------------------------------------------------------
136259cd  2026-10-18 16:09:58  lorna-laptop              oldest daily snapshot    /home/lorna/bench/data  5.547 GiB
                                                         oldest weekly snapshot
                                                         oldest monthly snapshot
16776abc  2026-10-18 16:19:48  lorna-laptop              daily snapshot           /home/lorna/bench/data  5.645 GiB
                                                         weekly snapshot
                                                         monthly snapshot
-------------------------------------------------------------------------------------------------------------------
Timestamps shown in local time
2 snapshots

remove 1 snapshots:
ID        Time                 Host          Tags        Paths                   Size
------------------------------------------------------------------------------------------
4ecaa305  2026-10-18 16:18:54  lorna-laptop              /home/lorna/bench/data  5.547 GiB
------------------------------------------------------------------------------------------

The prune that --prune runs afterwards reported total prune: 0 blobs / 0 B, because the unchanged snapshot held no data of its own. Remember that Borg only frees disk space at borg compact, so a prune on its own leaves the repository the same size. Kopia works differently: the policy is stored in the repository for each path, and Kopia enforces it itself at the end of every kopia snapshot create, deleting whatever the policy no longer keeps. Set the policy for the path you back up, list the snapshots, and preview with kopia snapshot expire, which only reports what it would delete unless you add --delete:

kopia policy set "$DATA" --keep-latest 10 --keep-hourly 0 --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --keep-annual 0
kopia snapshot list "$DATA"
kopia snapshot expire "$DATA"

Kopia’s default policy also keeps the 10 latest snapshots, so all three of ours stayed. Its snapshot list folds the unchanged run into the first one and shows which rule keeps each snapshot:

lorna@lorna-laptop:/home/lorna/bench/data
  2026-10-18 16:12:12 UTC k1562b50a1486b04e3b2c93c4b97eea2c 6 GB drwxrwxr-x files:86919 dirs:5760 (latest-2..3)
  + 1 identical snapshots until 2026-10-18 16:19:01 UTC
  2026-10-18 16:20:06 UTC kbaa8d136b6d4c2d90402520fbd278f7b 6.1 GB drwxrwxr-x files:86944 dirs:5760 (latest-1,daily-1,weekly-1,monthly-1)

The preview printed Nothing to delete for lorna@lorna-laptop:/home/lorna/bench/data. because the keep-latest rule still covers all three. Preview any tighter policy the same way, because there’s no dry run on the next snapshot. We checked that on a scratch folder with three snapshots: after setting --keep-latest 2 with every other rule at 0, kopia snapshot expire reported 1 snapshot(s) of lorna@lorna-laptop:/home/lorna/kopia-demo would be deleted. Pass --delete to do it., and the next kopia snapshot create deleted the two oldest without being asked, leaving only these:

lorna@lorna-laptop:/home/lorna/kopia-demo
  2026-10-18 17:29:56 UTC k7e8b30724f51bfd822fabb4a25a29a82 6 B drwxrwxr-x files:1 dirs:1 (latest-2)
  2026-10-18 17:29:59 UTC kc11dd06f8e302f39e7ecda787794bc33 5 B drwxrwxr-x files:1 dirs:1 (latest-1)

If you want Kopia to behave like the other two, lower --keep-latest. If you want Borg or Restic to keep a few recent snapshots regardless of date, add --keep-last to their commands.

Fix the errors you’re likely to hit

We reproduced one error per tool while working through this, and each has a short fix. All three come from small setup slips rather than bugs.

borg init: error: the following arguments are required: -e/–encryption

Borg 1.4 has no default encryption mode, so a bare borg init /srv/backup/borg-test prints its usage text, ends with this line and exits with status 2:

borg init: error: the following arguments are required: -e/--encryption

Add --encryption repokey as in the repository step. The mode can’t be changed after the repository exists, so choose it once.

Fatal: an empty password is not allowed by default

Restic prints this when RESTIC_PASSWORD isn’t set and there’s no terminal to prompt on, which is exactly what happens in a cron job or a systemd unit that doesn’t carry your shell’s exports. We reproduced it with env -u RESTIC_PASSWORD restic -r /srv/backup/restic snapshots < /dev/null, which exited with status 1:

Fatal: an empty password is not allowed by default. Pass the flag `--insecure-no-password` to restic to disable this check

Don’t take the flag it suggests for an encrypted repository. Set the password in the job’s environment instead, the same way you exported it in your shell.

open repository: repository is not connected

Kopia keeps its connection in ~/.config/kopia/repository.config for the user who created it. Running sudo kopia snapshot list looks in root’s home instead, finds nothing and exits with status 1:

open repository: repository is not connected. See https://kopia.io/docs/repositories/

Run Kopia as the same user every time. Don’t work around it by connecting root to the same repository: kopia repository status shows "fileMode": 384 (0600) for this repository, so files root writes there would belong to root and your normal user could no longer read them. If you need root to read files your user can’t, give root a repository of its own.

Decide whether Borg 2 is worth trying yet

Not for the backups you depend on. Borg 2 is still published as a beta on GitHub, and the project README says in capitals not to use it for production backups. There is no upgrade path from one beta to the next either, so you’d have to delete and recreate a beta repository every time. It also can’t read Borg 1.x repositories; moving archives across means borg transfer into a new repository. We ran the newest beta through the same benchmark as a side test, installed next to Borg 1.4 under the name borg2:

curl -sLO https://github.com/borgbackup/borg/releases/download/2.0.0b25/borg-linux-glibc239-x86_64-gh.tgz # pin: beta, check https://github.com/borgbackup/borg/releases
sudo mkdir -p /opt/borg2
sudo tar -xzf borg-linux-glibc239-x86_64-gh.tgz -C /opt/borg2
sudo ln -sf /opt/borg2/borg-dir/borg.exe /usr/local/bin/borg2
borg2 -r /srv/backup/borg2 repo-create -e aes256-ocb
borg2 -r /srv/backup/borg2 create --stats run-1 "$DATA"

If you’ve used Borg 1.4, the command line will look different. These are the changes you’ll notice first:

  • You pass the repository with -r instead of as a positional argument.
  • You create a repository with repo-create instead of init.
  • You choose the cipher (aes256-ocb or chacha20-poly1305) with -e and where the key lives with --key-location, which still defaults to repokey.
  • You name archives without the :: syntax.

On the same three runs, the beta compared with Borg 1.4 like this:

Median of 3 runsBorg 1.4Borg 2 beta
First backup (s)9693
Second backup, nothing changed (s)2971
Backup after the change (s)2770
Full restore (s)6156
Full data check (s)3328
Repository after first backup (GB)2.132.15
Peak memory, full restore (MiB)122372

First backups, restores and repository size came out close to Borg 1.4. Every incremental run took about 71 seconds whether or not anything had changed, though, more than twice as long as Borg 1.4, and the beta used roughly three times the memory during a restore. That’s a snapshot of one beta, not a verdict on the final release. If you’re curious, run it on a test repository next to your Borg 1.4 backups, and stay on 1.4 for anything you’d need to restore.

Pick the backup tool that fits you

Every row in this table comes from the medians above, measured on one Debian 13 VM writing to a local disk. We didn’t test cloud or SSH backends, the Kopia web UI or Vorta, so treat the table as a verdict on local repositories:

If you care most aboutPickWhat we measured
Smallest repositoryRestic1.85 GB, against 1.88 GB for Kopia with zstd and 2.13 GB for Borg’s lz4
Fast nightly incrementalsRestic or Kopia6.0 s and 5.7 s with nothing changed, against 29 s for Borg (16.8 s once the newest file was a small one)
Fast full restoreRestic or Borg52 s and 61 s, against 899 s for default Kopia (292 s in a cold re-run later the same day)
Low memory in full backups and restoresBorg119 MiB peak in the first backup and 122 MiB in a full restore; Restic used less in no-change runs, checks and the one-file restore
Quick full data checksRestic17.9 s to read and verify every pack
Retention without writing a policyKopiaA keep-latest, hourly, daily, weekly, monthly and annual policy is defined the moment the repository exists

If you’re still unsure, match your situation to one of these:

  • You want one tool that does everything well and you back up to a USB disk or a second local drive: pick Restic, because it led or tied on almost every row here.
  • You run full backups and restores on a small VM, a Raspberry Pi or a busy host where memory is tight: pick Borg, and pair it with Borgmatic for scheduling and retention.
  • You want a policy engine and a GUI more than raw restore speed: pick Kopia, turn on zstd before the first snapshot and time a full restore on your own disk before you rely on it.
  • You need the backups to survive ransomware on the backed-up host: whichever tool you choose, put the repository behind an append-only or object-lock target, as in our immutable backup setup with MinIO.

For databases and VM images, a file-level backup is only as good as the state of the files when it ran. Take an LVM snapshot for a consistent copy first and back up the snapshot, so a half-written database file never ends up as your only copy.

Once you’ve picked a tool, move from this local test to a real schedule and an off-site copy. Our guide to Restic backups to S3 storage covers the cloud side for Restic, and you can follow the same pattern for the others.

If Kopia is your pick, its Linux and Windows setup guide walks through KopiaUI and the repository server, the parts we left out here.

Keep reading

UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 Security UFW Firewall Commands with Examples on Ubuntu 26.04 / 24.04 PHP2Shell (CVE-2026-109618, CVE-2026-109619): Check and Patch PHP Php PHP2Shell (CVE-2026-109618, CVE-2026-109619): Check and Patch PHP PHP2Shell on End-of-Life PHP 8.1, 8.0 and 7.4 (WHMCS, ionCube) Php PHP2Shell on End-of-Life PHP 8.1, 8.0 and 7.4 (WHMCS, ionCube) Upgrade a Single Package on Ubuntu / Debian with apt Debian Upgrade a Single Package on Ubuntu / Debian with apt APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Cheat Sheets APT Cheat Sheet: apt and apt-get Commands for Ubuntu/Debian Best Hot-Swap Drive Cages and 5.25-Inch Bay Adapters for NAS Storage Best Hot-Swap Drive Cages and 5.25-Inch Bay Adapters for NAS

2 thoughts on “Borg vs Restic vs Kopia: Backup Tools Benchmarked”

Leave a Comment

Press ESC to close