One cron handles both halves. The multipart half is one call. The single-PUT half needs your app’s help, because the SDK cannot tell an abandoned object from a completed one by looking at the bucket. Only your own records can.
The simplest fix: always multipart
Sweeping incomplete multipart uploads
vercel.json
Single PUT: the pending row
If you keep the single-PUT path, write a row before the bytes, flip it after them, and sweep what never flipped from the same cron.1
Insert a pending row in the before-upload callback
It runs once per upload, before anything is signed, and already knows the path.
2
Flip it to ready as the last thing the completion callback does
Do all other work first. If the flip came earlier, a crash after it would leave a ready row whose work never happened.
3
Sweep rows still pending from the same cron, after the abort
An indexed query on your own table, not a bucket scan. Each row names the exact path to check.
The upload id first reaches your code in the completion callback, so the example mints its own row id in the before-upload callback and passes it as state, which reaches the completion callback.
The cron
Add the row sweep to the route above. The abort runs first, and both halves share one window.- One path, one upload. The sweep deletes whatever stands at a pending row’s path, so a path must never be shared. Unique paths give you that. With a reused path, a retry that succeeded leaves a ready row and an older pending row pointing at the same object, and the sweep would delete the good file. If you must reuse paths, skip rows whose path also has a ready row.
- Abort first, one window. A route without multipart always on still uses parts for a file over the threshold, so a pending row can belong to a paused multipart upload with nothing at its path yet. Running the abort first with the same window means the parts are gone before the row is, so a late resume fails instead of completing against a row that no longer exists. A row sweep on a shorter window would delete rows out from under uploads that can still resume.
Without a pending row
list carries no metadata, so it cannot tell which upload wrote an object, and it pages through every object under the prefix to find the few that do not belong. Use it only when a pending row is not an option.
Related traps
- Do not let the completion callback throw on a database error. Any throw deletes the object, and the browser’s retried
endthen reports a 404. See Retries and throws. - Use unique paths unless overwriting is the intent. Two single-PUT uploads to the same path race, and the loser’s
endfails withnot_foundeven though its bytes landed. A unique path is the fix; see Unique paths. - An explicit
cancel()is already handled. It tells your route, which aborts the multipart upload or deletes the single-PUT object. The gap is everything that is not an explicit cancel: a crash, a closed tab, a lost network.