S3 incomplete multipart uploads: How to Find Them and What They Cost
S3 incomplete multipart uploads are parts of failed large uploads, billed as storage and invisible in the console and in the bucket listing. They are one of the most reliable sources of pure waste in an AWS account, because nothing in the console flags them and nothing fails when they exist — the bill simply stays higher than it needs to be.
This page covers what they cost, the command that lists yours, and — importantly — what to check before deleting anything.
What they are and why they linger
Uploads larger than a few megabytes are split into parts and reassembled by a final CompleteMultipartUpload call. If the upload is interrupted — a crashed process, a cancelled CI job, a dropped connection — the parts already uploaded stay in the bucket, billed as storage, forever.
They do not appear in aws s3 ls, they do not appear in the console’s object listing, and they do not appear in S3 Inventory. They are visible only through the ListMultipartUploads API. This is the most genuinely hidden storage cost in AWS.
What they cost
Entirely dependent on how much large-object traffic fails, which makes it easy to dismiss and occasionally enormous. Backup jobs and data pipelines moving multi-gigabyte objects can leave terabytes of orphaned parts over a few years.
The tell is a bucket whose BucketSizeBytes CloudWatch metric is much larger than the total size of its listed objects. That gap is usually multipart parts, or noncurrent versions.
Finding yours
# Incomplete uploads in one bucket.
aws s3api list-multipart-uploads --bucket my-bucket \
--query 'Uploads[].{Key:Key,Initiated:Initiated,Id:UploadId}' --output table
# Across every bucket in the account.
for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
n=$(aws s3api list-multipart-uploads --bucket "$b" \
--query 'length(Uploads)' --output text 2>/dev/null)
[ "$n" != "None" ] && [ -n "$n" ] && [ "$n" != "0" ] && echo "$b: $n incomplete uploads"
done
Compare the metric against the listing to size the gap:
aws cloudwatch get-metric-statistics --namespace AWS/S3 --metric-name BucketSizeBytes \
--dimensions Name=BucketName,Value=my-bucket Name=StorageType,Value=StandardStorage \
--start-time "$(date -u -d '2 days ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --period 86400 --statistics Average
Before you delete
An upload initiated in the last few hours may still be in progress. Aborting it fails a live job. Use an age threshold of at least a day, and preferably seven.
Beyond that, incomplete parts have no recovery value — they cannot be assembled into an object without the original uploader’s part list.
Stopping them coming back
Add a lifecycle rule to every bucket. This is one of the few pieces of AWS advice with no downside and no exceptions:
{
"Rules": [{
"ID": "abort-incomplete-multipart-uploads",
"Status": "Enabled",
"Filter": {},
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}]
}
aws s3api put-bucket-lifecycle-configuration --bucket my-bucket \
--lifecycle-configuration file://lifecycle.json
Make it part of whatever module creates buckets, so no future bucket is without it.
Related
- S3 noncurrent versions with no lifecycle rule — every previous version of every object, retained forever because versioning was enabled without an expiry policy
- CloudWatch log groups that never expire — log groups created with the default retention of Never Expire, accumulating ingestion and storage charges indefinitely
- The whole cost-waste checklist — every category in this series
- NAT gateway cost calculator — model the largest recurring VPC charge
Prices quoted are us-east-1 list rates, last checked on 2026-08-22, and are shown with the arithmetic so you can substitute the rates for your own region. Always confirm against the AWS pricing page before acting on a number.