# Deleting orphaned blobs in Vercel Blob

> Blobs nobody references still bill every month. Where orphans come from, how to delete them in the same code path as the row, and a safe sweep with list() and del() for the rest.

Storage grows. Users do not. Somewhere, blobs are piling up that no row points
at.

Vercel Blob bills the monthly average of your store size. An orphan costs the
same as a file someone uses.

## Where orphans come from

1. **Replaced files.** A user uploads a new avatar. The row gets the new key.
   The old blob stays.
2. **Deleted rows.** A message is deleted. Its attachment is not.
3. **Abandoned uploads.** The bytes land, then the tab closes before the key
   is saved.
4. **Failed saves.** The upload works, the database write throws.

## Fix the first two in code

Delete the blob in the same path that drops the reference. Blob first, then
row:

```ts
import { del } from "@vercel/blob";

export async function deleteMessage(id: string) {
  const message = await getMessage(id);
  if (message.attachmentKey) await del(message.attachmentKey);
  await removeMessage(id);
}
```

Why that order: if the row delete fails after the blob is gone, the page shows
a missing file, which you will notice. The other order leaves a blob nobody
will ever find again.

`del()` is free (it is not billed as an operation), takes a pathname or URL,
and accepts an array. Make cleanup retry-safe: if a delete reports
`BlobNotFoundError`, the blob is already gone, which is the outcome you wanted.

For replacements, save the new key, then delete the old one. Not before: if
the save fails you still have the old file.

## Sweep the rest

Abandoned uploads and failed saves need a periodic sweep:

```ts
import { del, list } from "@vercel/blob";

const GRACE_MS = 24 * 60 * 60 * 1000;
const referenced = await loadReferencedKeys(); // every key column, first

let cursor: string | undefined;
const orphans: string[] = [];
do {
  const page = await list({ cursor, limit: 1000 });
  for (const blob of page.blobs) {
    const old = Date.now() - blob.uploadedAt.getTime() > GRACE_MS;
    if (old && !referenced.has(blob.pathname)) orphans.push(blob.pathname);
  }
  cursor = page.hasMore ? page.cursor : undefined;
} while (cursor);

for (let i = 0; i < orphans.length; i += 100) await del(orphans.slice(i, i + 100));
```

The parts that matter:

- **A grace period.** A blob from a minute ago with no row is a save in
  flight. A day is a safe floor.
- **Read the references before listing.** A row saved mid-run can then only
  protect a blob, never expose one.
- **Dry run first.** Print the count. If it is a large share of the store, you
  missed a column that holds keys.
- **Mind the costs and limits.** Each `list()` call is one advanced operation,
  so use `limit: 1000`. Each deleted blob counts toward the operation rate
  limit, even inside one `del([...])` call.

## Make orphans cheap to find

Put the owner first in every pathname: `<userId>/<folder>/<uuid>-<name>`.
Then "everything this user uploaded" is `list({ prefix: "<userId>/" })`, and
deleting an account's files is one loop, not a table scan.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
