uploadBlob stores the object and inserts it into the blobs table before anything actually references it
(blob.rs).
As far as I can tell, there is no code that removes a blob once its last reference disappears. Blob storage is only cleaned up when an account is deleted (scheduled.rs).
There is a record_blobs table which looks intended to track which blobs are referenced by records, but it is only populated by importRepo and a one-shot startup backfill. apply_commit does not update it, and deleteRecord explicitly passes blob_cids: &[]
(delete.rs).
So after the instance starts handling normal writes, record_blobs quickly stops reflecting reality.
The existing compaction and reachability_walk jobs don't solve this. Both require block_store.as_tranquil_store()
(scheduled.rs),
which means that with repo_backend = "postgres" they run but effectively do nothing, despite startup logging compaction_enabled=true reachability_enabled=true. They also deal with repo blocks, not blobs.
There isn't really an operator-side workaround either. listBlobs returns the contents of the blobs table directly
(postgres/blob.rs),
including blobs which are no longer referenced. So comparing listBlobs against the object store cannot be used to find these orphans.
The reference PDS removes dereferenced blobs. Tranquil currently doesn't seem to, so blob storage will keep growing over time. A simple example is replacing an avatar: every previous avatar remains in the object store.
The ATProto blobs spec (https://atproto.com/specs/blob#blob-lifecycle) specifies that after deleting all records that reference a blob, it should be deleted after a grace period. So tranquil isn't spec compliant.