Sanity dataset full of unused assets and old drafts: a safe cleanup

By Nayan Kyada · · 6 min read

Part of The Sanity + Next.js Guide

If your Sanity dataset has years of orphaned images, abandoned drafts and test documents nobody remembers creating, the Studio media browser crawls, exports take forever, and nobody trusts what is safe to touch. This is one of the most common things I find on inherited Sanity projects, and a Sanity dataset cleanup is usually a one-day job when done in the right order.

Symptoms of a cluttered Sanity dataset

None of these will take your site down. They just make every future change slower and riskier:

  • The Studio media library takes a long time to load, and editors cannot find the image they uploaded yesterday among thousands of near-duplicates.
  • Document lists are full of items titled "test", "copy of homepage" or "untitled".
  • Dataset exports and migration scripts take far longer than the visible content would suggest.
  • Nobody knows whether a document type is still used. I cover that side separately in auditing and pruning unused Sanity document types.
  • Editors keep duplicating old pages instead of starting fresh, because they do not trust the existing ones.

If the main complaint is a slow public site rather than a slow Studio, the cause is probably elsewhere. Start with how to find the cause of a slow Sanity website.

What clutter costs if you ignore it

The public pages rarely suffer. Your front end only requests assets that published documents reference. The cost shows up elsewhere:

  • Editor time. Searching a bloated media library and second-guessing which page is real adds up every week.
  • Migration risk. Any future move, whether a schema restructure or a platform change, has to carry all the junk with it. See restructuring a schema on a live site.
  • Plan limits. Asset storage and dataset size can count toward your Sanity plan. Check current limits on the Sanity pricing page rather than trusting a blog post, including this one.
  • Accidental publishing. Old drafts with forgotten content occasionally get published by someone who thought they were editing the current version.

Step 1: take a backup before touching anything

Export the dataset first with the Sanity CLI, for example npx sanity dataset export production from your Studio project. Keep the archive somewhere outside the repo. Everything below is reversible only because of this file, so do not skip it.

Step 2: find orphaned assets

Images and files in Sanity are stored as their own documents. An asset is orphaned when no other document references it. This query lists image assets with zero references:

*[_type == 'sanity.imageAsset' && count(*[references(^._id)]) == 0]{
  _id,
  originalFilename,
  size,
  _createdAt
} | order(_createdAt asc)

Run it in the Vision tool inside Studio. Swap the type for sanity.fileAsset to check PDFs and other files. Sort by size to see where the real weight is.

Before deleting, check three things the query cannot see:

  1. Hardcoded CDN URLs. If an email template, an old site or a printed QR flow points at the asset URL directly, deleting breaks it.
  2. Weak references and plain string IDs. Some custom fields store asset IDs as text, which references() does not count.
  3. Third-party plugins. Some Studio plugins store asset IDs in their own formats. Search your schema folder for custom asset handling first.

Step 3: delete in small batches

Do this from a script, not by clicking through Studio. Delete in batches so one failure does not abort a thousand operations.

// scripts/delete-orphan-assets.ts
import {createClient} from '@sanity/client'
 
const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID!,
  dataset: process.env.SANITY_DATASET ?? 'production',
  apiVersion: '2025-01-01',
  token: process.env.SANITY_WRITE_TOKEN!,
  useCdn: false,
})
 
const DRY_RUN = process.env.DRY_RUN !== 'false'
 
async function main() {
  const ids: string[] = await client.fetch(
    `*[_type == 'sanity.imageAsset' && count(*[references(^._id)]) == 0]._id`
  )
  console.log(`Found ${ids.length} orphaned image assets`)
  if (DRY_RUN) return
 
  for (let i = 0; i < ids.length; i += 100) {
    const batch = ids.slice(i, i + 100)
    const tx = client.transaction()
    batch.forEach((id) => tx.delete(id))
    await tx.commit()
    console.log(`Deleted ${i + batch.length} of ${ids.length}`)
  }
}
 
main().catch((err) => {
  console.error(err)
  process.exit(1)
})

Run it once with the default dry run and compare the count with what Vision showed you. Only then run with DRY_RUN=false. Use a token with the minimum permissions needed and revoke it afterwards.

Step 4: deal with stale drafts and test documents

Drafts are documents whose IDs start with drafts.. A draft that is months old and unchanged from its published version is noise. A draft with no published counterpart is either a never-published page or a forgotten experiment, and that is a judgement call, not a script.

My rule is simple: list them, ask an editor to sort the list into keep, publish or delete, and only then act. Automating this step is how people lose real work. Old test documents with obvious titles can go straight to the delete list once an editor confirms.

Step 5: verify nothing broke

  • Re-run the orphan query and confirm it returns nothing or only what you chose to keep.
  • Crawl the live site, or at least your top 50 URLs, and check for broken images.
  • Open a handful of pages in preview mode, since draft content can reference assets the published version does not.
  • Ask editors to confirm their recent work is intact.

If images are missing after cleanup, restore from the export. That is the reason it exists.

What to ask a developer before they delete anything

  • Where is the export stored, and who has access?
  • Will you run a dry run and share the list?
  • How do you handle assets referenced outside the dataset?
  • Who decides what happens to unpublished drafts?
  • Do you also stop the clutter at the source, for example by adding slug and title validation and archive workflows? See an unpublish and archive workflow in Sanity.

A developer who answers these quickly has done this before. One who offers to just run a delete script has not.

Prevention matters more than cleanup

Cleanup without process change gets you a clean dataset for about six months. Add a naming convention for test content, restrict who can create certain document types, and schedule a quarterly orphan check. Sorting this out is usually part of a broader Next.js website audit.

This is work I do. If your Sanity project feels heavier than its content justifies, my Performance & Sanity audit starts from $800 and covers dataset health alongside speed and schema review. Details are on the services page.

Frequently asked questions

How do I find unused images in a Sanity dataset?

Query image asset documents and filter for ones that no other document references, using a GROQ count over the references function. Review the list before deleting anything, because weak references and assets used outside the dataset will not show up as references.

Is it safe to delete orphaned assets in Sanity?

It is safe if you export the dataset first, delete in small batches, and confirm that no external site hardcodes the asset CDN URLs. Sanity will usually refuse to delete an asset that a document still strongly references.

Do unused Sanity assets slow down my website?

Not directly, since the front end only requests assets that documents point to. They do slow down the Studio media browser, inflate exports and migrations, and can count toward plan limits, so cleanup is about editor speed and cost rather than page speed.