DEV Community

Cover image for I Have 91 Backups. None of Them Are This Machine's.
Mustafa ERBAY
Mustafa ERBAY

Posted on Originally published at mustafaerbay.com.tr

I Have 91 Backups. None of Them Are This Machine's.

I was actually looking at something else. I opened ~/Backups to check whether a side
project's nightly dumps were still landing, saw the listing, and relaxed — 91 files, dates
marching along neatly. Then, in the same terminal, I typed a command I had not planned to
type: tmutil destinationinfo.

The answer was one line.

tmutil: No destinations configured.
Enter fullscreen mode Exit fullscreen mode

What came after made me write this article twice. In the first version I said "I have no
backup," and I believed I had proved it. Then I opened the places I had looked at too
quickly: I did have a backup. It covered exactly my most irreplaceable files, in fact —
and I had forgotten about it.

So the thesis here is not "I neglected my backups." It's the more uncomfortable one:
I didn't know what my backup covered. I had a working copy, a dead copy, and a pile of
things with no copy at all, and the map in my head got all three wrong. Every number below
was measured on this machine on 1 October 2026.

The backup that works: 91 dumps, 760 megabytes, zero surprises

Good news first. Inside ~/Library/LaunchAgents there is a job called
com.burncpu.backup-pull that wakes up every day at 10:00. What it does is modest:

rsync -az --timeout=60 vps3:/opt/burncpu/backups/ "$DEST/files/"
find "$DEST/files" -name "burncpu-*.sql.gz" -mtime +90 -delete
Enter fullscreen mode Exit fullscreen mode

It mirrors the nightly dumps from the server to this Mac; it never deletes to match the
source, it only prunes anything older than 90 days locally. The result: 91 .sql.gz
files
, 760 MB in total. The oldest is from 3 July, the newest is
burncpu-2026-10-01T00-00-02Z.sql.gz. Its log is dry and clear:

[2026-09-30 10:00:05] pull basliyor
[2026-09-30 10:00:22] ok — 91 dosya, 767M
[2026-10-01 10:00:05] pull basliyor
[2026-10-01 10:00:15] ok — 91 dosya, 761M
Enter fullscreen mode Exit fullscreen mode

I misread this job's health at first glance. The launchd.err.log next to it was dated 11
June, and I thought I had found a backup that had been stale for four months. But the file
is 0 bytes: it was born in June when the job was installed and has found nothing to write
since. The staleness of the error log was proof of health, not of failure.

That was a preview of the rest of this article: the same silence carries both good and bad
news, and I read it wrong both times on the first pass.

One detail: those 91 dumps are not a backup for this machine. Their original lives on
VPS3; the copy here is the second instance. If the Mac dies, that data lives on.

The machine's own backup: nothing at the system level

tmutil destinationinfo can't find a configured destination. tmutil listlocalsnapshots /
prints an empty heading and falls silent. diskutil list shows a single physical disk:
/dev/disk0, carrying a 460 GiB (494.4 GB) main APFS container; under /Volumes there is
nothing but the system's own boot volume. The data volume is 90% full, 386 GiB in use.

The tooling side says the same: restic, borg, borgmatic, kopia, duplicacy, arq
— none installed, no backup application under /Applications.

I need a parenthesis here, because I fell straight into a hole I dug myself. In my first
pass I ran du on the iCloud folder, saw 60 KB, and wrote "sync is off, the data is
here." Wrong. The folder holds 211 files and every one of them has blocks set to 0 —
they're uploaded to iCloud and evicted locally. du counts blocks, so it reports 60 KB.
The Desktop and Documents entries inside it aren't "empty shells" either; they're
symbolic links to the real folders, and du doesn't follow links, so they show as 0 bytes.
What I took as proof that sync was off was proof that it was working.

A week ago I wrote
a separate article
about how the "free space" number on this very disk changes depending on who you ask. I
forgot that article's lesson in my own measurement one week later. This seems likely to
keep happening, so I'm writing it down here.

I didn't forget to turn Time Machine on — I have no disk to turn it on with

Apple's documentation sets two conditions for Time Machine. Capacity: a device with "at
least twice the storage capacity of your Mac." Dedication: you use that disk only for Time
Machine. Twice a 460 GiB disk means, roughly, a 1 TB external drive — and it means that
drive exists solely for this purpose. I don't own one. So what's missing isn't a setting,
it's hardware.

Local snapshots don't close the gap either. As far as Apple's page says, they're kept on
the Mac's own startup disk, taken roughly every hour, and held for 24 hours. A copy that
lives on the same disk is no help when the disk itself is what goes.

My list is empty. I don't know exactly why, and I won't present my guess as fact: the
destination may simply never have been configured, or Apple's other condition on the same
page may not hold — it says Time Machine "stores snapshots only on disks that have plenty
of free space," and my data volume is 90% full. Two candidate causes; my data isn't enough
to choose between them.

What would I lose: 37 repos, 209 commits

"I have no backup" is dramatic on its own, and empty. What matters is how much
irreplaceable material sits on that single disk. ~/IdeaProjects is 35 GB. I counted.

Measurement Count
Git repositories (.git directory, two levels deep) 37
Repositories with no remote (git remote) at all 9
Of those, ones with real history 8
Commits in those 8 repositories 209
Tracked files in those 8 repositories 1,332
Repositories ahead of their remote (unpushed) 4
Unpushed commits 143
Dirty repositories 14
Modified files 275
Untracked entries / actual files 230 / 1,518

I'm writing the counting method down so it can be checked: I searched for .git
directories, which leaves four git worktrees out of the list — their commits already
live in the main repository.

I also have to correct that "230," because my first pass had a units error in it: 230 is
the number of entries git collapses, so most of them are directories, not individual
files. The real untracked file count is 1,518. And that number shouldn't be taken at
face value either: one empty scratch repository alone accounts for 1,118 of them, another
for 196. So most of the 1,518 is noise. The real work is in the places I sampled: a new
cost-rollup module, a barcode handler, six new test files, and database migrations numbered
from the tenth through the twenty-second. Migrations are the kind of file that takes the
schema with it when it disappears.

I'm not writing the repository names down; most of them are client work. But I do need to
mention one: a 916 MB portal, 19 commits, its last two commits made on 29 September, 44
minutes apart. So it's live work from days ago. Remotes configured: zero. One of those
commit messages contains this: "restore the quote package and pricing work that was lost in
production." The code that rescues work lost in production is not itself anywhere it could
be rescued from.

Let me say exactly what the check covers when I say "no remote": git remote returns
empty, meaning there is no configured path to push to. That doesn't prove the code was
never copied anywhere — as we're about to see, some of it was. But on a recovery night, "I
think I copied it somewhere" doesn't count as a plan.

Where git doesn't look — and where it looks by accident

I counted the .env style files across the projects: 18. Then I asked each one, "is git
tracking you?" On my first attempt the answer was "none," and I was pleased with that. My
method was broken: I asked about each file by its bare name instead of its path relative to
the repository root, so git naturally found nothing.

Asked properly: 16 are untracked, 2 are tracked.

The repositories holding those two have an .env rule in their .gitignore. So the rule
was written and the file is inside anyway — added before the rule, or force-added. Their
contents are two lines of domain and API address, not a secret leak; but that isn't the
point.

The point is this: what git protects is what git looks at, and I was wrong about both
sides of that boundary. 16 files sit outside the scope, by my own choice, despite being the
hardest class to reproduce. 2 files sit inside it while I believed they were out. I can
rewrite a lost source file; I cannot rewrite a lost key — I'd have to go to the provider,
generate a new one, and redeploy it everywhere it was used.

The destination I named "backup" is dead — the one I didn't name works

Hoping for better, I looked at the rclone side. Two remotes are configured:

gdrive:
gdrive-yedek:
Enter fullscreen mode Exit fullscreen mode

The second one is named, literally, "yedek" — Turkish for backup. I tried listing it:

CRITICAL: Failed to create file system for "gdrive-yedek:":
couldn't find root directory ID: ... couldn't fetch token:
invalid_grant: maybe token expired? - try refreshing it
Enter fullscreen mode Exit fullscreen mode

In the first version of this article I stopped right here. I wrote "the destination I named
backup cannot authenticate," closed the section, and that was drama built on selective
evidence. I saw two remotes and tested only the broken one.

When I opened the other, seven folders came back. Three of them matter:

  • A "Projeler" folder dated 22–30 April: ~100,000 objects, 5.6 GiB. Two branches underneath, lokal and harici, with six project folders under lokal.
  • A folder dated 18 September: Android release signing material and a restore note. A signing key is even less replaceable than an API key.
  • A key escrow dated 11 July: GPG-encrypted, 1,788 bytes.

So an offsite copy exists, encryption exists, a restore note exists. My
sentence "I have no copy anywhere" was wrong, and I had to delete it.

But the real finding starts here. I compared the names of the six project folders in that
April copy against the list of nine repositories with no remote: none of them match.
Worse, the earliest first commit among those nine is 7 July. So the copy sitting in the
cloud was taken ten weeks before all the work I had flagged as at risk. However
well-intentioned, that folder does not protect me — what it would need to protect hadn't
been born yet.

Diagram

This is the best evidence for the "silence is the default" thesis. Nobody told me anything
false; I never asked anyone the right question. The destination with "backup" in its name
misled me precisely because it had a name; the one without a name escaped me precisely
because it didn't. An inventory should be kept by measurement, not by label.

I also don't know why the token died. Google's documentation lists several causes for
invalid_grant: access being revoked, a refresh token going unused for six months, a
password change while Gmail scopes are granted, exceeding the limit of 100 tokens per
account — in which case, in the documentation's own words, the oldest are invalidated
without warning — and, for projects whose publishing status is "Testing," expiry in 7
days. I can't say which. What every item shares is that none of them is obliged to tell me.

I've caught this pattern before. A launchd job
tried 21,881 times and failed every time,
and I never looked once.

Today, for the first time, I tested a dump

Last question: can those 91 dumps I called good actually be restored?

I searched the pulling script for restore, verify, gunzip -t, zcat, psql,
pg_restore. Zero hits. The script copies and prunes; it does not test. For ninety-one
days files arrived, and not one was ever opened.

Today I tested the newest. gzip -t passed, and zcat gave up its first lines:

--
-- PostgreSQL database dump
--
Enter fullscreen mode Exit fullscreen mode

That proves one thing: the dump's stream was not truncated. A pg_dump | gzip that
died halfway would fail its checksum. That's all I know, and I won't claim more. gzip -t
verifies the integrity of the compression container, not the consistency of the SQL inside
it; a missing role, a missing extension, a broken ordering would all be invisible.
PostgreSQL's documentation, describing a plain-text dump, says what to do with one: to
restore it, you feed it to psql. A dump becomes a backup only when a database eats it.

CISA's backup guidance asks for three things: 3-2-1 (three copies, two different media, one
offsite), protecting the backups themselves (physical security, encryption, offline
copies), and testing the procedures. Let me draw my own scorecard honestly:

Copies Offsite Encrypted Tested
Side project database 2 yes no never
April project copy 2 yes no never
Key escrow 2 yes yes never
Work since 7 July 1 no — —

Here's what's interesting: the best job I did is in the smallest file. A 1,788-byte GPG
escrow covers two of the three legs at once. The rest was built out of that day's urgency,
not out of habit.

What I'm going to do, ordered by risk

  1. Add remotes to the 9 repositories that have none. 209 commits stop being single-copy in exchange for one evening's work. Client code goes to private repositories. Cheapest action, biggest gain.
  2. Push the 143 unpushed commits, decide about the 275 modified files. Migrations first. Most of the 1,518 untracked files are junk, but I can't know that without sorting them.
  3. Keep the inventory by measurement, not by label. This is the real lesson of this article. I didn't know what the April copy covered; unless I write down what today's copy covers, I won't know that either in three months.
  4. Either refresh the dead token or delete it. A non-working path with "backup" in its name is worse than no path at all — judging by the fact that it made me write this article twice, it did the same to me.
  5. Buy a 1 TB disk and turn Time Machine on. And remember that a backup disk left permanently connected is also exposed to ransomware; CISA's "offline copies" item exists exactly for that.

An honest note: I did not do item 1 today. If I don't do it tomorrow, this paragraph will
be sitting here to embarrass me.

Backup isn't a tool question, it's an inventory question

What I take from this measurement isn't "I neglected my backups" — that's what I thought
when I first wrote it, and I was wrong. I did make backups. There is a daily, automatic, 91-file one; a 5.6 GiB copy of my projects went to the cloud; a key escrow got encrypted with GPG. I couldn't remember what any of them covered.

Because the thing that's easy to automate is not the thing that's expensive to lose. The
database on the server dumps with one command and already exists in two places. A
half-finished migration, an uncommitted test, a key shown exactly once: none of them dumps
with one command. Backup effort ran downhill, like water, along the path of least
resistance — and nobody reminded me where that path ended up.

Back in June I published an article on this blog arguing that backups are everyone's
responsibility. While writing it, I had not run tmutil destinationinfo on my own machine.
The distance between those two things bothers me, so I'm putting it in writing: giving
advice is easier than measuring.

The default sound a system makes is silence. Time Machine didn't report having no
destination, the token didn't announce its death, the April copy didn't mention going
stale. And today I read a 0-byte error log first as a fault and then as proof of health,
read iCloud's evicted files as "sync is off," and wrote the sentence "I have no copy
anywhere" only to delete it. I misread the same silence three times in one day.

So don't ask your own setup "do I have a backup?" You'll answer that one with a clear
conscience. Ask this instead: what exactly does my backup cover, up to what date — and how
do I know? I asked today, and I didn't know half the answer.

Official Sources

Top comments (2)

Collapse
 
ndcodes profile image
Nnamdi Felix Ibe •

Great post 🙌. The folder named "yedek" was dead, and the unnamed one worked. You trusted the label and ignored the thing actually doing the job.

I've just checked my own machine after reading this, and I'd have given you a confident answer about my coverage five minutes ago. I was wrong about roughly half of it.

"Giving advice is easier than measuring" is the line. Writing the honest version twice is rarer than the audit itself. 👊

Collapse
 
merbayerp profile image
Mustafa ERBAY •

That might be the best result I could get from this post. 😄

If it made you actually check your own machine instead of just thinking “yeah, backups are important,” then the article did its job.

And the funny part is that I was confidently wrong several times while auditing mine. 😂 The more I measured, the more the map in my head fell apart.

I think that’s the part I’ll keep from this: don’t ask “do I have backups?” Ask “show me exactly what I can restore, from when, and prove it.”

Glad I made you check yours too. 👊