ListMultipartUploads Behavior and Design Trade-Offs
This note documents the ListMultipartUploads design across its full engineering lifecycle: the current MinIO-compatible contract in Server 20261018, the October 18 partial rollback after 16-drive erasure-set capacity testing, the October 17 on-disk scan experiment (#243, merged as 7e9454456), the September 16 opt-in strict mode (#198), and the original August 30 root-cause analysis.
Current Contract in Server 20261018
Server 20261018 restores the traditional MinIO-compatible public ListMultipartUploads contract on main (commit 4733e1423, followed by empty-upload-parent cleanup repair 0f9199e0c):
- Without
prefix,ListMultipartUploadsreturns uploads currently tracked in the node-local in-memory multipart cache. - With a non-empty
prefix,ListMultipartUploadstreatsprefixas an exact object key (sha256(bucket/object)) and checks the multipart directory for that object on disk. delimiter,key-marker,upload-id-marker, andencoding-typeare parsed by the handler, validated where required (encoding-type=url), and echoed in the XML response, while candidate selection follows the MinIO-compatible cache / exact-key path rather than a full.minio.sys/multipartdisk scan.UploadID,GetMultipartInfo,PutObjectPart,ListObjectParts,CompleteMultipartUpload, andAbortMultipartUploadcontinue to work byBucket + Object + UploadIDacross restarts because active uploads live on disk under.minio.sys/multipart/<sha256(bucket/object)>/<uploadID>.MINIO_API_MULTIPART_LISTINGandapi multipart_listingremain accepted as compatibility no-ops if present in older scripts or stored configs.GET /minio/admin/v3/multipart-preflightremains available and reportsmode: "legacy"with0incompatible uploads.
Why the Default Disk Scan Was Rolled Back on 2026-10-18
Between 2026-10-17 (#243, commit 4ed86dfe6) and 2026-10-18 (commit 4733e1423), main briefly used an unindexed .minio.sys/multipart disk scan for all public ListMultipartUploads calls. Capacity analysis and benchmarks on 16-drive erasure sets showed that making this scan the unconditional default created a worse operational failure mode than the listing limitation it replaced.
Why Restart Never Deleted Multipart Uploads
Active multipart uploads are stored on disk under .minio.sys/multipart/<sha256(bucket/object)>/<uploadID>/xl.meta and part.*. A server restart clears only the in-memory multipartUploadsCache; it does not remove unfinished uploads from disk. Retained UploadID values therefore remain usable across restarts until completed, explicitly aborted, or cleaned up by the background stale-upload sweep (default expiry: 7 days; check interval: 12 hours).
Because unfinished uploads accumulate across restarts and client retries, any operation that walks .minio.sys/multipart on disk scales with the total number of unfinished uploads currently stored in the erasure set, not merely the uploads created since the last process start.
How the 100,000-Entry Guard Fails on a 16-Drive Erasure Set
The October 17 disk scan enforced maxMultipartWalkEntries = 100000 across all drives in an erasure set combined, counting both <sha256(bucket/object)> directories and <uploadID> directories on every drive.
On an N-drive erasure set with U active uploads across H distinct keys, a single bucket-wide ListMultipartUploads call traverses:
When each upload uses a distinct object key ($H = U$):
- On a 4-drive set, $4 \times (10{,}000 + 10{,}000) = 80{,}000$ directory entries—just under the
100,000budget. - On a 16-drive set, $16 \times (3{,}126 + 3{,}126) = 100{,}032$ directory entries—exceeding the
100,000budget at 3,126 active uploads across the entire erasure set (all buckets combined) and failing every public scan with HTTP503 SlowDown. - Even when all uploads share a single object key ($H = 1$), a 16-drive set reaches $16 \times (1 + 6{,}250) = 100{,}016$ entries at 6,250 uploads for that key, so even an exact-key lookup (
prefix=<exact-object-key>) failed with503 SlowDown.
Provenance and Responsibility
| Change | Date / Commit | What It Did | Assessment |
|---|---|---|---|
Initial strict mode (#198) |
2026-09-16 / 4cbb074cc |
Added opt-in disk-backed listing behind MINIO_API_MULTIPART_LISTING=strict, keeping legacy as default |
Safe opt-in boundary, but lacked a hard traversal cap |
Security hardening (SN-2026-009..012) |
2026-09-16 / 143f6970d |
Added checkUploadID, maxMultipartWalkEntries = 100000, 503 SlowDown mapping, and readQuorum filtering |
Essential DoS protection for the disk scan, but counted raw per-drive directory entries across all N drives |
Unconditional unification (#243) |
2026-10-17 / 4ed86dfe6 |
Removed legacy and made the disk scan unconditional for all public ListMultipartUploads calls |
Overreach: turned a 4-drive benchmark result into a mandatory path that fails at 3,126 uploads on 16-drive sets |
Partial Rollback Trade-Off Analysis
| Option | Public S3 Listing Fidelity | 16-Drive Capacity Behavior | Storage Format / Background I/O Impact | Decision |
|---|---|---|---|---|
Keep #243 unconditional disk scan |
Full prefix/delimiter/marker listing up to 3,125 uploads on 16 drives | Fails with 503 SlowDown at 3,126 distinct-key uploads across the set |
16 × (1 + H) ListDir RPCs per uncached public listing |
Rejected |
Raise or remove maxMultipartWalkEntries |
Works past 3,126 uploads | Reopens unbounded O($N \times U$) disk walk and memory allocation on unindexed .minio.sys/multipart |
High latency and FD/RPC amplification under concurrent GET /bucket?uploads |
Rejected |
| Build a persistent per-bucket multipart index | Full S3 listing without sha256(bucket/object) full scan |
Scales with matching prefix | Requires a new distributed metadata index, heal protocol, and crash-recovery contract | Deferred (requires dedicated index design) |
Full git revert of #198, 143f6970d, and #243 |
Restores MinIO-compatible listing | No 100,000-entry cliff |
Would also discard checkUploadID path-traversal protection, abort cache/peer cleanup, and xl.meta identity metadata |
Rejected |
Surgical partial rollback (4733e1423 + 0f9199e0c) |
Restores MinIO-compatible public listing (empty-prefix cache + non-empty exact-key lookup) | No cross-bucket 100,000-entry cliff; exact-key lookup touches only 1 hash directory per drive |
Preserves xl.meta identity fields, checkUploadID, abort cleanup, and empty-hash-parent removal |
Selected |
What Was Retained vs Rolled Back
- Rolled back on the public listing path (
4733e1423):- Public
ListMultipartUploadsno longer walks every<sha256(bucket/object)>directory in.minio.sys/multipart. - Empty
prefixreads the in-memorymultipartUploadsCache; non-emptyprefixhashesbucket/prefixdirectly and inspects only that single object’s<sha256(bucket/object)>directory.
- Public
- Retained hardening and storage repairs (
4733e1423+0f9199e0c):NewMultipartUploadstill writesx-minio-internal-multipart-v1-bucketandx-minio-internal-multipart-v1-objectinxl.meta, keeping on-disk uploads self-describing for diagnostics and future indexed designs.checkUploadIDvalidation remains enforced on multipart operations (SN-2026-009).AbortMultipartUploadstill evicts the local cache entry and notifies peer nodes.cleanupStaleUploadscontinues to remove expired uploads on disk, and commit0f9199e0censures that when the last<uploadID>under.minio.sys/multipart/<sha256(bucket/object)>is removed by expiry sweep, completion, or abort, the empty<sha256(bucket/object)>parent directory is also removed so empty hash directories do not accumulate indefinitely.
Cost of Ordering vs Discovery
In .minio.sys/multipart/<sha256(bucket/object)>/<uploadID>, directory names are SHA-256 hashes of bucket/object, which destroys lexicographical key order and bucket boundaries. Answering an arbitrary prefix query (prefix=trips/) in S3 key order without an index requires reading xl.meta inside every active upload across every bucket in the erasure set before sorting and paginating the first page. By contrast, exact-key lookup (prefix=trips/2026/a.mp4) computes a single SHA-256 hash and lists only that one directory per drive (O(N) ListDir calls instead of O(N × H)).
Validation Benchmarks on Commit 4733e1423
Measured with go test -run=^$ -bench=BenchmarkMultipartPublicListingCapacity -benchmem -benchtime=1x ./cmd:
Scenario (BenchmarkMultipartPublicListingCapacity) |
Before (4ed86dfe6 Disk Scan) |
After (4733e1423 MinIO-Compatible Path) |
Latency (4733e1423) |
Memory / Allocs (4733e1423) |
|---|---|---|---|---|
4_drives_10000_distinct_keys/empty_prefix_cached |
200 OK (walked 80,000 entries on disk) |
200 OK (1,000 uploads returned from cache) |
1.12 ms |
257,104 B / 14 allocs |
16_drives_3126_distinct_keys/empty_prefix_cached |
503 SlowDown (100,032 entries > 100,000 cap) |
200 OK (1,000 uploads returned from cache) |
0.36 ms |
90,208 B / 14 allocs |
16_drives_3126_distinct_keys/exact_key_after_cache_clear |
503 SlowDown (walked all 3,126 hash dirs) |
200 OK (1 upload returned from disk after cache clear) |
0.78 ms |
30,024 B / 389 allocs |
16_drives_10000_uploads_one_key/exact_key_lookup |
503 SlowDown (at 6,250 uploads for one key) |
200 OK (1,000 uploads returned from disk) |
383.85 ms |
13,721,672 B / 236,704 allocs |
Related Records
| Document | Scope |
|---|---|
| S3 API Compatibility — Differences from S3 APIs for Multipart Uploads | Canonical user-facing compatibility reference for ListMultipartUploads in Server 20261018 |
| SILO 20260916 Release Notes | Published legacy default, opt-in strict behavior, UploadID validation, and cancellation boundaries |
Historical Record: The October 17 Unification Attempt (#243)
On 2026-10-17, PR #243 (commit 4ed86dfe6) briefly removed the legacy/strict mode switch and routed all ListMultipartUploads requests through the disk-backed implementation introduced in #198. Although rolled back the next day by 4733e1423, the architectural analysis from #243 is preserved below because it documents the exact storage format, quorum rules, and abort-race edge cases of .minio.sys/multipart.
October 17 Architectural Decision Record
Target Contract of the October 17 Experiment
The October 17 experiment attempted to enforce the following invariants on ListMultipartUploads without introducing a separate metadata database:
- Restart durability:
ListMultipartUploadsreads active uploads from.minio.sys/multiparton disk rather than relying exclusively onmultipartUploadsCache. - S3 filter and pagination semantics:
prefixis matched as a string prefix of the object key,delimitercollapses common prefixes intoCommonPrefixes, and results are ordered by(Key, Initiated, UploadID)withkey-markerandupload-id-markerpagination. - Self-describing upload metadata:
NewMultipartUploadstores the bucket and object name inside each upload’sxl.metaso on-disk uploads can be mapped back to(Bucket, Object)despite the<sha256(bucket/object)>directory layout.
Why Unification Was Attempted
Before #198, callers saw very different results depending on whether prefix was empty, a partial key prefix, or a full object key:
| Request Example | Traditional MinIO / legacy Behavior |
S3 Standard / Disk-Scan Behavior |
|---|---|---|
GET /photos?uploads (immediately after CreateMultipartUpload) |
Returns uploads present in the local node’s in-memory cache | Returns active uploads present at read quorum on disk |
GET /photos?uploads (after server restart) |
Returns empty list (multipartUploadsCache was cleared on restart) |
Still returns active unfinished uploads from .minio.sys/multipart |
GET /photos?uploads&prefix=2026/ (directory prefix) |
Returns empty list unless an object is literally named 2026/ (sha256("photos/2026/")) |
Returns all active uploads whose key starts with 2026/ |
GET /photos?uploads&prefix=2026/trip.mov (exact object key) |
Looks up .minio.sys/multipart/<sha256("photos/2026/trip.mov")> on disk (survives restart) |
Returns uploads whose key starts with 2026/trip.mov |
Issues #198 (2026-09-16) and #213 initially kept both behaviors behind MINIO_API_MULTIPART_LISTING=legacy|strict so upgrades with pre-existing legacy uploads would not break silently. PR #243 (2026-10-17) sought to eliminate the two-mode configuration surface after adding lazy backfill for older uploads.
Alternatives Evaluated During #243
| Candidate | Restart Survival | Prefix & Marker Support | Extra Index Maintenance | Why Chosen / Why It Later Failed |
|---|---|---|---|---|
1. Keep legacy + strict dual mode |
Only in strict |
Only in strict |
None | Rejected in #243 to remove mode split; revisited in 4733e1423 |
| 2. In-memory cache only | No | Yes (while cached) | None | Rejected: loses all visibility on restart |
3. Scan .minio.sys/multipart + xl.meta identity (#243) |
Yes | Yes | None | Chosen in #243; rolled back in 4733e1423 because O($N \times (H+U)$) walk hits 100k cap at 3,126 uploads on 16 drives |
| 4. Sidecar JSON file per upload | Yes | Yes | Extra file create/delete per upload | Rejected: doubles metadata file operations on create/abort |
| 5. Per-bucket on-disk multipart index tree | Yes | Yes (O(matching prefix)) | Requires distributed index heal & cleanup | Deferred: only viable long-term path for large erasure sets, but requires its own crash/heal protocol |
| 6. External KV / SQLite index | Yes | Yes | External state outside erasure coding | Rejected: violates self-contained erasure-set architecture |
On-Disk Storage Layout and xl.meta Identity Fields
An active multipart upload is stored at:
Because <sha256(bucket/object)> is a one-way digest, the directory path alone does not reveal the bucket or object name. Commit 4cbb074cc extended newUploadedObjectMetadata in cmd/erasure-multipart.go to store two internal metadata keys in xl.meta (retained in 20261018):
x-minio-internal-multipart-v1-bucket: plain bucket namex-minio-internal-multipart-v1-object: plain object key
Compatibility check against upstream MessagePack xl.meta readers (verified against commit 3be10fcc1a44f6620ded0bd303461f9d688cca23): MetaSys is a standard map[string][]byte inside xl.meta, so older readers ignore unknown x-minio-internal-* keys without schema errors.
Discovery and Quorum Rules in the Disk-Scan Path
When the disk-scan path (listMultipartUploadsScan) ran in #198 and #243, each erasure set executed five steps:
- List
<sha256(bucket/object)>directories under.minio.sys/multipartacross all online drives, requiring each hash directory to appear on at leastreadQuorum = floor(N/2) + 1drives. - List
<uploadID>subdirectories inside each quorum-present hash directory, validating everyuploadIDwithcheckUploadIDand requiringreadQuorumpresence. - Read
xl.metafrom online drives for each candidateuploadIDand require at leastreadQuorumdrives to agree on(Bucket, Object). - If an older pre-upgrade upload lacked
x-minio-internal-multipart-v1-*keys, resolve(Bucket, Object)when an exact prefix or cache entry supplied the candidate key and lazily backfillxl.meta. - Filter by
Bucket,Prefix,Delimiter,KeyMarker, andUploadIDMarker, sort by(Key, Initiated, UploadID), and merge across pools.
Directory Traversal Cost and 4-Drive Benchmark (#243)
On an N-drive erasure set with H active <sha256(bucket/object)> directories and U active <uploadID> directories across all buckets:
- Total directory entries inspected per uncached scan: $N \times (H + U)$
- Total
ListDiroperations per uncached scan: $N \times (1 + H)$
In #243, BenchmarkMultipartListingCapacity was run only on a 4-drive single-set fixture (N = 4), where 10,000 uploads for a single key ($H = 1, U = 10{,}000$) produced $4 \times (1 + 10{,}000) = 40{,}004$ directory entries:
4-Drive Fixture (BenchmarkMultipartListingCapacity on 4ed86dfe6) |
Active Uploads | Wall Time | Memory Allocated | Allocations |
|---|---|---|---|---|
| Single object key ($H = 1, U = 10{,}000, N = 4$) | 10,000 |
101.6 ms |
3.58 MB |
61,210 |
That single-key, 4-drive benchmark masked the two multiplicative factors that broke on 16-drive sets the following day: $N = 16$ drives ($4\times$ multiplier) and distinct object keys $H = U$ ($2\times$ entry multiplier and $U\times$ ListDir multiplier), which together pushed 3,126 uploads over maxMultipartWalkEntries = 100000.
Abort-vs-Delayed-Create Race on 16-Drive EC:8 Sets
During the #243 review, we also analyzed whether AbortMultipartUpload could leave behind a resurrected upload under a fixed readQuorum = floor(N/2) + 1 (for example, 9 on a 16-drive EC:8 set):
- Suppose
NewMultipartUploadwritesxl.metaon8drives immediately while7drives are stalled in flight and1drive is offline. - A concurrent
AbortMultipartUploaddeletes the8existing copies on the fast drives and returns. - The
7delayedNewMultipartUploadwrites then land on disk, and the1offline drive later heals or comes back with its copy—leaving8readable copies (readQuorumforEC:8object reads) of an upload that the client already aborted.
While AbortMultipartUpload cache eviction and peer notification (retained in 4733e1423) keep the in-memory cache consistent across nodes, eliminating delayed-create resurrection on disk would require tombstones or per-upload generation locks.
Summary of October 17 Code Changes (Later Partially Reverted)
cmd/object-api-interface.go,cmd/server-main.go,cmd/config-current.go: madeapi multipart_listinga compatibility no-op (4ed86dfe6; retained as no-op in4733e1423).cmd/erasure-multipart.go: added lazyxl.metaidentity backfill during exact-key resolution and routed all listings through the disk scanner (4ed86dfe6; public scan routing reverted in4733e1423,xl.metawrite metadata and0f9199e0cempty-parent cleanup retained).
October 17 Test Evidence
go test ./cmd -run 'TestMultipartUploadListing|TestMultipartPreflight|TestMultipartListingConfig'passed on4ed86dfe6.BenchmarkMultipartListingCapacitypassed at101.6 mson a 4-drive single-key fixture, which proved insufficient to catch the 16-drive distinct-key limit.
Follow-Up Lessons from #243
Any future attempt to provide restart-durable arbitrary-prefix ListMultipartUploads must index active uploads by (bucket, object, uploadID) rather than scanning .minio.sys/multipart/<sha256(bucket/object)> directories across all drives in the erasure set, and must benchmark capacity on 16-drive sets with distinct object keys ($H = U$).
Historical Record: September 16 Opt-In strict Mode (#198)
Issue #198 (commit 4cbb074cc, hardened in 143f6970d and 0ff5b37fc) delivered the initial opt-in implementation for Server 20260916:
- Added
MINIO_API_MULTIPART_LISTING=legacy|strict(defaultlegacy) andmcli admin config set/reset ALIAS api multipart_listing. - Added
GET /minio/admin/v3/multipart-preflightso operators could check whether pre-existing.minio.sys/multipartdirectories lackedx-minio-internal-multipart-v1-bucketandx-minio-internal-multipart-v1-objectbefore enablingstrict. - Enforced S3 ordering by
(Key, Initiated, encoded UploadID)andfloor(N/2) + 1drive discovery quorum instrictmode. - Observed during September 16 Docker Desktop testing:
10,000active uploads across4containers took18.7 s(p95 = 2.68 sat concurrency10) due to per-drivexl.metareads over virtualized container filesystems.
Original Root-Cause Analysis (2026-08-30)
Overview
In upstream MinIO’s erasure-coded storage engine (erasureObjects), CreateMultipartUpload stores each upload on disk under .minio.sys/multipart/<sha256(bucket/object)>/<uploadID>, while also recording the upload in an in-memory per-bucket cache (multipartUploadsCache). Because the on-disk directory name hashes bucket/object with SHA-256, the storage layer can jump directly to a known object’s multipart directory when given the exact bucket and object, but cannot list uploads by bucket or by key prefix from disk without opening every active upload directory.
Original August 30 Recommendation
The August 30 investigation recommended fixing ListMultipartUploads in two stages: first writing (bucket, object) identity metadata into xl.meta at NewMultipartUpload time and exposing an opt-in disk-backed mode (#198), and later evaluating whether the disk scan or a dedicated per-bucket index should become the default.
Evidence and Sources
AWS S3 Specification
The AWS S3 ListMultipartUploads specification requires filtering by prefix and delimiter, lexicographical ordering by Key and Initiated time (with UploadId breaking ties), and continuation via key-marker and upload-id-marker.
Repository Issue #198
Issue #198 reported that ListMultipartUploads with a directory prefix (prefix=dir/) returned an empty list even immediately after CreateMultipartUpload, and that unprefixed ListMultipartUploads returned an empty list after a server restart even though UploadPart and CompleteMultipartUpload using the saved UploadId still succeeded.
Upstream MinIO History
- MinIO PR
#5248: Replaced the earlier bucket-mirrored multipart tree with.minio.sys/multipart/<sha256(bucket/object)>/<uploadID>to avoid deep path-length limits and empty parent-directory cleanup bugs on POSIX filesystems. - MinIO PR
#20407: Added the in-memorymultipartUploadsCacheso empty-prefixListMultipartUploadscould return recently created uploads without walking disk, while routing non-emptyprefixdirectly tolistMultipartUploads(bucket, prefix, ...)as an exact object key lookup. - MinIO Issue
#20989: Documented the resulting upstream limitation indocs/minio-limits.md:ListMultipartUploadsis only supported with an empty prefix (from memory) or withprefixset to the full object key.
Code Inspection in pgsty/silo
cmd/bucket-handlers.go(ListMultipartUploadsHandler): extractedprefix,keyMarker,uploadIDMarker,delimiter, andmaxUploads, validatedencoding-type, and passed them toobjectAPI.ListMultipartUploads.cmd/erasure-sets.go(erasureSets.ListMultipartUploads): branched onprefix == ""—readingmultipartUploadsCache.List(bucket)when empty, and hashingpathJoin(bucket, prefix)to callset.listMultipartUploadson a single disk hash directory when non-empty.cmd/erasure-multipart.go(erasureObjects.listMultipartUploads): listed<uploadID>directories under.minio.sys/multipart/<sha256(bucket/object)>, ignoringdelimiter,keyMarker, anduploadIDMarker.cmd/erasure-server-pool.go(erasureServerPools.ListMultipartUploads): iterated across pools and returned early on the first pool that produced a non-empty result whenprefix != "".
Design Review Findings
The initial code review confirmed that the defect was architectural rather than a simple parser typo: the HTTP handler parsed every S3 query parameter, while the storage layer lacked an index mapping bucket + key-prefix to <sha256(bucket/object)>.
Behavior Before #198 (and Restored Default in 20261018)
Empty prefix Path
When prefix == "":
erasureSets.ListMultipartUploadsreads the in-memorymultipartUploadsCacheforbucket.- Uploads created before the last process restart are absent from the cache, even though their
.minio.sys/multipart/<sha256(bucket/object)>/<uploadID>directories remain intact on disk. - Cache synchronization across distributed nodes is best-effort (
updateMultipartUploadInfo).
Non-Empty prefix Path
When prefix != "":
erasureSets.ListMultipartUploadstreatsprefixas a full object name (object = prefix), hashesbucket/objectwith SHA-256 to locate the target erasure set, and reads.minio.sys/multipart/<sha256(bucket/object)>on disk.- If a caller passes a true key prefix such as
prefix=photos/2026/, the server hashes the literal string"bucket/photos/2026/", finds no upload for an object with that exact name, and returns zero uploads. - If a caller passes the exact object key
prefix=photos/2026/video.mp4, the server hashes"bucket/photos/2026/video.mp4", reads the matching<uploadID>directories from disk, and returns all active uploads for that object even after a server restart.
Multi-Pool Early Return
In erasureServerPools.ListMultipartUploads, the non-empty prefix path makes sense for an exact object key because sha256(bucket/object) maps deterministically to one erasure set in one pool, whereas true prefix listing across multiple pools would have needed to merge results from every pool.
Why Legacy Uploads Cannot Be Backfilled Automatically Without Candidate Keys
Because .minio.sys/multipart/<sha256(bucket/object)>/<uploadID>/xl.meta historically stored neither the bucket name nor the object key, an existing pre-20260916 upload directory on disk contains only a SHA-256 digest of bucket/object. Without either knowing the candidate bucket/object string (to verify its SHA-256 digest) or having x-minio-internal-multipart-v1-bucket and x-minio-internal-multipart-v1-object in xl.meta, the server cannot invert sha256(bucket/object) back to (Bucket, Object).
Structural Complexity
| Layer | What It Assumes | Consequence |
|---|---|---|
S3 HTTP API (ListMultipartUploadsHandler) |
prefix is a key prefix; delimiter groups common prefixes; markers paginate sorted keys |
Expects ordered (Key, Initiated, UploadID) candidates per bucket |
On-Disk Layout (.minio.sys/multipart/<sha256(bucket/object)>) |
Callers always supply the exact bucket and object (PutObjectPart, Complete, Abort) |
O(1) lookup by exact key, but no bucket partition and no lexicographical key order |
In-Memory Cache (multipartUploadsCache) |
Tracks uploads observed since process start | Fast O(bucket) listing for empty prefix, but ephemeral across restarts |
Operational Impact
Wire Compatibility Impact
SDKs and backup tools that call ListMultipartUploads with a directory prefix (such as aws s3api list-multipart-uploads --bucket B --prefix dir/) receive an empty list unless prefix equals the full object key.
Storage and Orphan Cleanup Impact
Tools that rely on ListMultipartUploads after a restart or with a directory prefix to discover and abort unfinished uploads cannot see those uploads via listing; however, Silo’s built-in background stale-upload sweep (cleanupStaleUploads) walks .minio.sys/multipart directly by modification time and cleans up expired uploads regardless of ListMultipartUploads behavior.
Operational Trade-Off
As proven on 2026-10-18, forcing ListMultipartUploads to walk .minio.sys/multipart on every uncached public request turns a listing limitation into a 503 SlowDown availability cliff at 3,126 active uploads on 16-drive erasure sets. Keeping the MinIO-compatible path protects cluster-wide ListMultipartUploads availability while preserving exact-key disk lookup and background expiry cleanup.
Severity Classification
- Data integrity risk: None (
PutObjectPart,CompleteMultipartUpload, andAbortMultipartUploadaddress uploads by exactBucket + Object + UploadID). - S3 API compatibility gap: Documented difference in S3 API Compatibility.
Design Options Compared
Option 0: Keep Traditional Behavior Without Documentation
Leaves callers surprised when prefix=dir/ returns empty results. Rejected in favor of explicit documentation and retained xl.meta identity metadata.
Option 1: Preserve MinIO-Compatible Behavior and Document the Contract (20261018 Choice)
Keeps empty-prefix listing backed by the in-memory cache and non-empty prefix backed by exact-key disk lookup, while keeping xl.meta self-describing and fixing empty hash-parent cleanup (0f9199e0c). Selected in 20261018 after the 16-drive scan regression.
Option 2: Scan .minio.sys/multipart on Disk with xl.meta Identity
Implemented as opt-in strict mode in #198 and briefly made default in #243. Works on small upload counts, but scales as $O(N \times (H + U))$ across all buckets in an erasure set and hits the 100,000-entry traversal cap at 3,126 distinct-key uploads on a 16-drive set.
Option 3: Dedicated Per-Bucket Multipart Index
Maintains a bucket-scoped, prefix-ordered metadata index alongside .minio.sys/multipart. Avoids cross-bucket SHA-256 directory scans, at the cost of maintaining and healing a second distributed metadata structure on every CreateMultipartUpload, CompleteMultipartUpload, AbortMultipartUpload, and expiry sweep.
Option 4: Expand In-Memory Cache to Support Prefix/Delimiter Only
Applies full S3 prefix, delimiter, and marker filtering to multipartUploadsCache in memory without disk scanning. Fast and bounded, but still loses visibility of pre-restart uploads until an exact-key operation touches them.
Reference Architecture for Disk-Backed Listing (Implemented in #198, Rolled Back on Public Path in 4733e1423)
1. Contract Definition
Define exact ordering (Key, Initiated, UploadID) and marker continuation rules before touching storage routines.
2. Write Path (xl.meta Identity)
Write x-minio-internal-multipart-v1-bucket and x-minio-internal-multipart-v1-object into xl.meta in NewMultipartUpload (retained in 20261018).
3. Read Path
Require readQuorum agreement across drives when reading multipart metadata from disk.
4. Semantic Filtering Layer
Apply Prefix, Delimiter, KeyMarker, UploadIDMarker, and MaxUploads above the per-set discovery layer.
5. Exact-Key Helper
Keep a direct O(1) exact-key lookup helper (sha256(bucket/object)) for internal callers and exact-key queries.
6. Cache Synchronization
Evict multipartUploadsCache entries on AbortMultipartUpload and CompleteMultipartUpload across peers (retained in 20261018).
7. Migration and Preflight
Provide /minio/admin/v3/multipart-preflight for inspecting on-disk multipart directories (retained as a compatibility endpoint in 20261018).
8. Degraded and Suspended Pools
Skip suspended pools cleanly during multi-pool listing rather than failing the entire request.
Spike Checklist Used for #198
- Verify
MetaSysMessagePack compatibility with existingxl.metareaders. - Measure
ListDirandreadAllamplification acrossNdrives. - Verify
cleanupStaleUploadsremoves empty<sha256(bucket/object)>parent directories (fixed in0f9199e0c).
Verification Gates
Unit Tests
Validate prefix, delimiter, marker pagination, and checkUploadID path-traversal rejection.
Handler and End-to-End Tests
Verify ListMultipartUploadsHandler response XML, URL encoding, and cache vs exact-key behavior across restarts.
Failure and Capacity Benchmarks
Run BenchmarkMultipartPublicListingCapacity across both 4-drive and 16-drive erasure-set topologies with up to 10,000 active uploads.
Delivery Gates
Document the public ListMultipartUploads contract in S3 API Compatibility and keep all historical anchors intact.
Current Engineering Conclusion
Keep the MinIO-compatible public ListMultipartUploads path (4733e1423) and empty-parent directory cleanup (0f9199e0c) as the production default in Server 20261018. Do not re-enable unindexed .minio.sys/multipart disk scans on the public listing path; if full restart-durable prefix listing is required in a future release, design a bucket-scoped multipart index benchmarked on 16-drive erasure sets first.