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:
- Hardcoded CDN URLs. If an email template, an old site or a printed QR flow points at the asset URL directly, deleting breaks it.
- Weak references and plain string IDs. Some custom fields store asset IDs as text, which
references()does not count. - 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.