Unlike the other S3 findings, this one is not about how your objects are stored. It is about storage you are paying for that is not an object at all.
What triggers this finding
An S3 bucket with at least 1 GB of abandoned multipart upload parts and no lifecycle rule to abort incomplete uploads after a set number of days.
Typical fix
Add an `AbortIncompleteMultipartUpload` lifecycle rule (e.g. after 7 days) and abort existing incomplete uploads. Paid plans include lifecycle scripts with replacement warnings.
Example savings
100% of storage consumed by abandoned parts — commonly $1–$50+/month; large uploads may be higher.
See also: Severity and savings estimates for how Parsivex calculates figures on your report.
Storage you are billed for but cannot list
Any upload larger than a few hundred megabytes is split into parts. S3 stores each part as it arrives, and assembles them into an object only when the client sends CompleteMultipartUpload. If that call never comes — a laptop closes mid-aws s3 cp, a CI runner is evicted, an SDK throws on a flaky connection, a container is rescheduled — the parts already uploaded stay exactly where they are. Forever. There is no default expiry.
Those parts bill as storage at the storage class of the upload, which Parsivex prices at the S3 Standard rate of $0.023 per GB-month. What makes them expensive out of proportion to their size is that nothing shows them to you: they do not appear in ListObjectsV2, they are absent from the console's object browser, and the bucket size you see does not include them. Teams routinely carry hundreds of gigabytes of parts from a migration that finished two years ago. S3 Storage Lens reports them explicitly as incomplete multipart upload bytes, and it is usually where people find out.
Parsivex flags a bucket when parts from uploads initiated more than seven days ago total at least 1 GB and the bucket has no enabled lifecycle rule with an AbortIncompleteMultipartUpload action. A disabled rule, or one with a zero-day setting, does not count as protection.
Listing abandoned uploads yourself
Every in-flight upload for a bucket, with the age that decides whether it is abandoned:
aws s3api list-multipart-uploads --bucket data-lake-raw \
--query 'sort_by(Uploads, &Initiated)[].[Initiated,Key,UploadId]' --output table
Sizing one upload means adding up its parts:
aws s3api list-parts --bucket data-lake-raw --key exports/2024-full.tar.gz \
--upload-id <upload-id> --query 'sum(Parts[].Size)'
This is also where Parsivex's own numbers become approximate, and the report says so. It sizes up to 100 uploads per bucket, samples up to 1,000 parts per upload, and reads at most ten pages of part listings before extrapolating from the average part size it has seen. Beyond those limits the total is marked as sampled. The bounds exist because ListParts is a billed request and a truly pathological bucket could otherwise cost real money to audit.
Uploads that are still in flight
The seven-day floor exists precisely so that normal uploads never appear here — a healthy multipart upload completes in minutes, and nothing initiated this week is ever counted. What the floor cannot distinguish is a genuinely slow upload from an abandoned one.
Legitimate uploads that outlive seven days do exist:
- Multi-terabyte transfers over a constrained link, where an on-premises pipe pushes parts for weeks.
- Paused and resumed migrations, deliberately throttled to protect bandwidth during business hours.
- Long-running restore or archive jobs that hold an upload open across a maintenance window.
Aborting one of those throws away everything transferred so far, and the client cannot resume — the upload ID becomes invalid immediately. Sort by Initiated and look at the keys before acting: a cluster of uploads from a single date is a failed job, while a steady trickle of recent parts under one key is a transfer in progress.
The sampled estimate is the second caveat. When a bucket mixes 5 GB uploads with 5 MB ones, extrapolating from an average part size can be off in either direction. The finding tells you it was sampled; if the number drives a decision, verify it with list-parts on the specific uploads.
Cost and risk of aborting
There are two separate actions here, and they carry different risk.
Adding the lifecycle rule is safe and forward-looking. AWS's own recommendation is seven days; set the window above your longest legitimate upload and it will never touch work in progress:
{
"Rules": [
{
"ID": "abort-incomplete-uploads",
"Status": "Enabled",
"Filter": { "Prefix": "" },
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
}
]
}
Because writing a lifecycle configuration replaces every rule on the bucket, include your existing rules in the same document — this is the failure mode Parsivex's remediation scripts warn about before they run.
Aborting an existing upload is immediate and irreversible. The parts are deleted, the upload ID stops working, and a client attempting to resume receives NoSuchUpload. There is no charge for aborting, and no way to recover the transferred bytes.
Note the timing gap between the two. The rule is evaluated on a daily cycle and applies from that point forward, so parts already sitting in the bucket keep billing until it runs. If you want this month's bill to drop, abort the known-dead uploads explicitly with aws s3api abort-multipart-upload and let the rule handle everything that comes after.