Context
Avatars are content-addressed: the NATS object is keyed by {owner_id}_{content_hash} and the serve route (GET /avatars/{kind}/{id}/{version}/) fetches that object directly — no DB lookup, immutable cache. On re-upload the service does: put(new) → update(avatar_url) → delete(old_version) (old hash parsed from the current avatar_url).
The residual
The ordering guarantees the DB URL always points at an existing object (no user-visible gap). But if the process crashes after the DB update and before delete(old), the previous version's object is left in NATS, unreferenced forever. Over a long-lived deployment with frequent avatar changes, these orphans accumulate unbounded.
- Severity: low — each orphan is a tiny (~≤ a few KB, ≤512px WebP) unreferenced object; only leaks on a crash during a re-upload; not user-visible.
- Not a concern pre-launch / at low avatar churn.
Why the obvious fix isn't in yet
The natural fix is a sweep-on-upload (after storing the new version, delete every {owner_id}_* object except the current). But NATS object-store list() has no server-side prefix filter — it lists the entire bucket. Sweeping on every upload would mean a full-bucket scan (every account's every avatar) per upload, which is too expensive. A cheap targeted "find by prefix" isn't available in the NATS API.
Options when this matters
- Background reaper — periodic task lists the bucket once and deletes any
{id}_* object not referenced by the DB. Amortizes the full-bucket scan away from the hot path. (Preferred if churn grows.)
- Sweep-on-upload — only viable if a prefix-filtered list is added to the NATS object-store wrapper (or accept the full scan for small buckets).
- Overwrite single object per owner — eliminates orphans but re-introduces a DB lookup on serve (loses DB-free serving) to validate the URL version. Rejected as it sacrifices the main design win.
Current state
Shipped with put → update → delete-old (single targeted delete, no listing). This handles the normal case with zero orphans; only a crash mid-rotation leaks one. Documented here as a known residual.
Context
Avatars are content-addressed: the NATS object is keyed by
{owner_id}_{content_hash}and the serve route (GET /avatars/{kind}/{id}/{version}/) fetches that object directly — no DB lookup, immutable cache. On re-upload the service does:put(new)→update(avatar_url)→delete(old_version)(old hash parsed from the currentavatar_url).The residual
The ordering guarantees the DB URL always points at an existing object (no user-visible gap). But if the process crashes after the DB update and before
delete(old), the previous version's object is left in NATS, unreferenced forever. Over a long-lived deployment with frequent avatar changes, these orphans accumulate unbounded.Why the obvious fix isn't in yet
The natural fix is a sweep-on-upload (after storing the new version, delete every
{owner_id}_*object except the current). But NATS object-storelist()has no server-side prefix filter — it lists the entire bucket. Sweeping on every upload would mean a full-bucket scan (every account's every avatar) per upload, which is too expensive. A cheap targeted "find by prefix" isn't available in the NATS API.Options when this matters
{id}_*object not referenced by the DB. Amortizes the full-bucket scan away from the hot path. (Preferred if churn grows.)Current state
Shipped with
put → update → delete-old(single targeted delete, no listing). This handles the normal case with zero orphans; only a crash mid-rotation leaks one. Documented here as a known residual.