Merge branch 'cowork/majormac/mastodon-prune-profiles-trap'

This commit is contained in:
Marcus Summers 2026-05-07 12:01:48 -04:00
commit 306e5f1f16
3 changed files with 161 additions and 1 deletions

View file

@ -0,0 +1,156 @@
---
title: Mastodon — The `--prune-profiles` Trap and How to Recover
description: Why running `tootctl media remove --prune-profiles` blows away avatars that don't come back, and how to repopulate them on demand
tags:
- mastodon
- tootctl
- federation
- self-hosting
- troubleshooting
created: 2026-05-07
updated: 2026-05-07
---
# Mastodon — The `--prune-profiles` Trap and How to Recover
If you administer a Mastodon instance and run `tootctl media remove --prune-profiles` on a schedule, you're probably introducing a long-running cosmetic regression that no one will be able to explain when it happens.
This article documents what the flag actually does, why the missing avatars don't auto-recover, and the smallest tool you can ship to fix things on demand.
## TL;DR
- `tootctl media remove --prune-profiles` deletes cached **remote** avatars older than `--days=N` from your S3/local storage **and** clears `accounts.avatar_file_name` in the database.
- Mastodon does **not** re-fetch avatars when a client views a profile. Re-fetch happens only on incoming `Update` ActivityPub activities or via an explicit `tootctl accounts refresh`.
- Quiet remote accounts therefore stay broken — sometimes for weeks — after a prune.
- The disk savings are modest (≈250 KB per account on average) and the cosmetic damage hits exactly the accounts you care about most: your follows.
- Most admins should **drop `--prune-profiles` and `--remove-headers` from cron** and refresh on demand instead.
## What the flags actually do
`tootctl media remove` has three distinct modes:
| Invocation | Target | Default `--days` |
|---|---|---|
| `tootctl media remove` | remote media **attachments** (images/video in posts) | 7 |
| `tootctl media remove --prune-profiles` | remote **avatars** | 7 |
| `tootctl media remove --remove-headers` | remote **headers** | 7 |
Each mode deletes the file from your storage backend and nullifies the corresponding `accounts.avatar_file_name` / `header_file_name` column. They are **mutually exclusive** — passing two at once produces:
```
--prune-profiles and --remove-headers should not be specified simultaneously
```
If your cron script combines them, **the avatar/header pruning silently never runs**, and the first time you correct the bug you'll suddenly nuke everything that's accumulated since the instance was created.
## Why the pictures don't come back
Mastodon's media-recovery model is event-driven, not lazy. The triggers that cause a remote avatar to be re-fetched are:
1. The remote actor emits an `Update` ActivityPub activity — typically when they edit their profile, change avatar, change display name, etc.
2. Less reliably, certain `Create` activities on accounts whose actor state appears stale.
3. Manual: `tootctl accounts refresh user@instance.tld`, the web UI's "Refresh profile" button (gear menu on the profile page), or admin actions touching the actor record.
What does **not** trigger a re-fetch:
- Loading the profile in any client (web, iOS app, Ivory, Tusky, Toot!, etc.).
- Liking, replying to, boosting, or following toots from the user.
- Viewing the user in your followers/following list.
This is why you see **broken avatars consistently across every client and device** — the asset is missing on your server, and your clients are all faithfully fetching from the same broken URL.
Active accounts re-emit `Update` activities reasonably often, so they self-heal over hours/days. Quiet accounts, accounts on small or down instances, and accounts whose owners simply don't update their profiles can stay broken indefinitely.
## Recovery on demand
Single account:
```bash
sudo -u mastodon -H bash -c '
cd /home/mastodon/live
export RAILS_ENV=production
export PATH=/home/mastodon/.rbenv/bin:/home/mastodon/.rbenv/shims:$PATH
bin/tootctl accounts refresh user@instance.tld
'
```
For your local user's follows, a small wrapper that finds only accounts with broken avatars *whose origin actually advertises one*:
```bash
#!/bin/bash
# refresh-my-follows.sh — repopulate broken avatars for the local user's
# follows. Idempotent. Skips accounts whose origin has no avatar (e.g.,
# users who never set one) and headers entirely (most users have none).
set -euo pipefail
export PATH="/home/mastodon/.rbenv/bin:/home/mastodon/.rbenv/shims:$PATH"
export RAILS_ENV=production
cd /home/mastodon/live
USER_TO_REFRESH="${1:-yourusername}"
accts=$(bin/rails runner "
acct = Account.find_by(username: %q($USER_TO_REFRESH), domain: nil)
abort %q(no such local account) unless acct
acct.following
.where.not(domain: nil)
.where(avatar_file_name: nil)
.where.not(avatar_remote_url: [nil, ''])
.pluck(:username, :domain)
.each { |u, d| puts %Q(#{u}@#{d}) }
" | grep -E '^[^[:space:]@]+@[^[:space:]@]+$' || true)
count=$(printf '%s\n' "$accts" | grep -cv '^$' || true)
echo "Found $count remote follows with missing avatar"
i=0
while IFS= read -r a; do
[ -z "$a" ] && continue
i=$((i+1))
printf '[%d/%d] refresh %s ... ' "$i" "$count" "$a"
if bin/tootctl accounts refresh "$a" >/dev/null 2>&1; then
echo OK
else
echo FAIL
fi
done <<< "$accts"
```
Three things in that WHERE clause matter:
- `avatar_file_name: nil` — local cache is empty, so we need to fetch.
- `domain: not nil` — only remote accounts have cached avatars to repopulate.
- `avatar_remote_url: [nil, '']` excluded — if the origin actor object has no avatar, refresh will not populate anything. Including these accounts puts the script in an infinite-retry loop on every run.
## Why `header_file_name IS NULL` is a bad signal
A naive script will treat both `avatar_file_name IS NULL` and `header_file_name IS NULL` as "broken." Don't.
Roughly 20% of Mastodon users never set a custom header — the default blank header isn't represented as a file, so `header_file_name` is legitimately `NULL` for them. After a `tootctl accounts refresh`, the field stays `NULL` because there is genuinely nothing to fetch. A script with `OR header_file_name IS NULL` will retry these accounts forever and never make progress.
Avatar is different — nearly all real users set one, so `avatar_file_name IS NULL AND avatar_remote_url IS NOT NULL` is a reliable "broken and fixable" signal.
## The cron decision
If your weekly media-prune cron currently looks like:
```bash
bin/tootctl media remove --days=7 --concurrency=5
bin/tootctl media remove --prune-profiles --days=7 --concurrency=5
bin/tootctl media remove --remove-headers --days=7 --concurrency=5
bin/tootctl preview_cards remove --days=30 --concurrency=5
```
Consider deleting the middle two lines. The attachment prune is the real disk-saver (gigabytes per week on a busy instance). The avatar prune is small (~250 KB per remote account) and damages your UX. The header prune is even smaller and rarely worth it.
## Edge cases
- **Origin-side 404:** the actor object advertises an avatar URL, but the URL itself returns 404. Your local cache stays empty no matter how many times you refresh. Only the origin user can fix it (re-upload). The script above will keep retrying these on every run; if that bothers you, add a "tried within last N hours" filter.
- **Suspended accounts:** `tootctl accounts refresh` returns OK on suspended accounts but does not download media. They'll stay broken, which is correct behavior.
- **Sidekiq backlog:** the avatar fetch is queued as a Sidekiq job, not done synchronously. If your `pull` queue is deep, you'll see a delay between "OK" and the avatar actually appearing in the database.
## Related
- [Mastodon Instance Tuning](mastodon-instance-tuning.md) — broader perf notes for self-hosters
- [Mastodon DB Maintenance](mastodon-db-maintenance.md) — what to run on a schedule and when
- [Mastodon Federation](mastodon-federation.md) — how the actor refresh fits into the larger federation model

View file

@ -36,6 +36,7 @@ updated: 2026-05-05T23:39
* [Netdata n8n Enriched Alert Emails](02-selfhosting/monitoring/netdata-n8n-enriched-alerts.md)
* [Updating n8n Running in Docker](02-selfhosting/services/updating-n8n-docker.md)
* [Mastodon Instance Tuning](02-selfhosting/services/mastodon-instance-tuning.md)
* [Mastodon — The `--prune-profiles` Trap and How to Recover](02-selfhosting/services/mastodon-prune-profiles-trap.md)
* [Ghost Email Configuration with Mailgun](02-selfhosting/services/ghost-smtp-mailgun-setup.md)
* [Claude Code Remote Control — Mobile Access to a Persistent Host Session](02-selfhosting/services/claude-code-remote-control.md)
* [Linux Server Hardening Checklist](02-selfhosting/security/linux-server-hardening-checklist.md)
@ -91,6 +92,7 @@ updated: 2026-05-05T23:39
* [Windows OpenSSH Server (sshd) Stops After Reboot](05-troubleshooting/networking/windows-sshd-stops-after-reboot.md)
* [Windows OpenSSH: WSL Default Shell Breaks Remote Commands](05-troubleshooting/networking/windows-openssh-wsl-default-shell-breaks-remote-commands.md)
* [Pi-hole AI Blocklist Blocks Claude Desktop (ERR_CONNECTION_REFUSED)](05-troubleshooting/networking/pihole-blocks-claude-desktop.md)
* [Claude Desktop MCP Server Started via wsl.exe Sees Empty Environment (WSLENV)](05-troubleshooting/wsl-env-claude-desktop-mcp.md)
* [Ollama Drops Off Tailscale When Mac Sleeps](05-troubleshooting/ollama-macos-sleep-tailscale-disconnect.md)
* [Ollama: `ollama run` with Piped Stdin Bypasses Chat Template + SYSTEM Prompt](05-troubleshooting/ollama-chat-template-pipe-stdin-bypass.md)
* [rsync over Tailscale: Hung in TCP Teardown After Transfer Completes](05-troubleshooting/networking/rsync-tailscale-teardown-stall.md)

View file

@ -1,6 +1,6 @@
---
created: 2026-04-06T09:52
updated: 2026-05-02T16:45
updated: 2026-05-02T17:50
---
# MajorLinux Tech Wiki — Index
@ -105,6 +105,7 @@ updated: 2026-05-02T16:45
- [Mastodon DB Maintenance — Statuses, Accounts, and VACUUM](02-selfhosting/services/mastodon-db-maintenance.md)
- [Mastodon Federation — Domain Blocks, Silencing, and FediSeer](02-selfhosting/services/mastodon-federation.md)
- [Mastodon Instance Tuning](02-selfhosting/services/mastodon-instance-tuning.md)
- [Mastodon — The `--prune-profiles` Trap and How to Recover](02-selfhosting/services/mastodon-prune-profiles-trap.md)
- [Updating n8n Running in Docker](02-selfhosting/services/updating-n8n-docker.md)
### Storage & Backup
@ -213,6 +214,7 @@ updated: 2026-05-02T16:45
| Date | Article | Domain |
|---|---|---|
| 2026-05-07 | [Mastodon — The `--prune-profiles` Trap and How to Recover](02-selfhosting/services/mastodon-prune-profiles-trap.md) | Self-Hosting |
| 2026-05-02 | [WSL2 Backup via PowerShell Scheduled Task](01-linux/distro-specific/wsl2-backup-powershell.md) | Linux |
| 2026-05-02 | [SSH Config and Key Management](01-linux/networking/ssh-config-key-management.md) | Linux |
| 2026-05-02 | [Wake-on-LAN via Router SSH](02-selfhosting/dns-networking/wake-on-lan-router-ssh.md) | Self-Hosting |