Purge Stale Uploads
Clean up uploads that were never confirmed, and prune old soft-deleted rows.
An upload that starts but never confirms (a closed tab, a network error)
leaves a pending row, and sometimes an object or a multipart upload in
S3. Each pending row expires with its upload ticket. After that it no
longer counts toward the quota, and s3.db.purgeStale cleans it up.
import { s3 } from "@/lib/s3";
const { purged, failed, more } = await s3.db.purgeStale();
console.log(`Purged ${purged} stale uploads, ${failed} failed.`);
if (more) console.log("More stale rows are waiting.");What purge does
purgeStale({ limit }) handles at most limit rows per run (default
100), oldest first. It returns purged (uploads cleaned up), failed
(rows that will be retried), skipped (rows another run claimed, or that
were confirmed meanwhile), and more, which is true when the run hit
limit. For each row past its deadline:
- It claims the row with a compare-and-set. A pending row becomes
purging. A row that was confirmed, deleted, or taken by another run in the meantime is skipped. - It aborts the multipart upload, if there is one.
- It runs HeadObject, and deletes the object only while it still carries
this upload's marker. The delete sends
If-Matchwith the ETag, so an object written after HeadObject survives. A provider that does not implementIf-Matchgets a plain delete. - It removes the row.
An expired overwrite keeps its active row and the previous object, unless the overwrite's object had already replaced it. Then the row goes too.
A row that fails stays claimed, and a warning goes to the dimah-s3 logger.
A row whose route has no S3 client (a route you removed from dimahS3())
fails the same way instead of leaving its object untracked: keep the route
until its rows are purged.
The claim lapses after 15 minutes, and a later run retries the row. An
upload to a key that purge has claimed gets CONFLICT until then.
The deadline is the upload ticket's expiry: by default one hour after the
presigned URL expires, and 24 hours for multipart uploads. Change it
with upload.confirmWindow and upload.multipart.ticketExpiresIn (see
upload tickets).
Prune deleted rows
With deleteMode: "soft", deleted rows stay for audit. pruneDeleted
removes the rows deleted before olderThan:
const DAY = 24 * 60 * 60 * 1000;
const { pruned } = await s3.db.pruneDeleted({
olderThan: new Date(Date.now() - 30 * DAY),
});olderThan is a Date or epoch milliseconds, not a duration.
Prop
Type
Frequently asked questions
Run it as a server-side scheduled job: a Vercel Cron, a GitHub Actions
workflow, or a Node cron worker. Hourly is a good start. When more is
true, more rows are waiting: run it again, or raise limit.
It helps. Purge only sees uploads that have a row. An
AbortIncompleteMultipartUpload rule also catches multipart uploads with no
row, such as those on routes with plugins: { db: false }.