How we store a 420,000-file browser upload as one object

An engineering note on bundles: why one file per uploaded file fell over at scale, how browser uploads now write straight into a single bundle at their final offsets, and what that did to finalize time and download speed on our test node.

RIPTON CLOUD team, Product and engineering · · 2 min read

In short

  • Storing browser uploads as one file per uploaded file made file count, not bytes, the cost: 420,000 creates, closes and renames on finalize.
  • A bundle is one object with a manifest and payload-relative offsets. Uploads now write batches directly into it at their final offsets; finalize is a rename.
  • On our test node, finalizing a 420,000-file upload went from about five minutes to under ten seconds, and archive builds for download read sequentially.

File count was the cost, not bytes

RIPTON Desktop has always packaged a directory as a single bundle before moving it. Browser uploads through Web Edge did not: each uploaded file landed on the node as its own file, in a staging area, and finalize moved each one into place. That is fine for a hundred files. We tested with a folder of about 420,000 small files, the shape of a source-asset tree or a tiled raster, and it was not fine.

  • Finalizing the upload took around five minutes on the test node: one create, write, close and rename per file, serialised on the filesystem's metadata path.
  • Building an archive for download had the same shape: 420,000 opens and reads, each a separate syscall path, each competing for IOPS with the uploads in flight.
  • Every session-scoped request hydrated the whole session, so a per-batch origin check on a session with 420,000 paths was itself expensive.

None of this was about network speed. The bytes were a few gigabytes. The work was in the count.

One object, one manifest, fixed offsets

A bundle is a single file with a short header, a manifest that lists every path with its size and payload-relative offset, and then the payload. Desktop already produced and consumed this format, so the node already knew how to read a byte range out of one.

RIPTBUNDLE\n
<20-digit manifest length>
<manifest: path, size, offset for every entry>
<payload: entry bytes, back to back, at the manifest's offsets>

The change for browser uploads was to assign every file its final offset at setup time, when the control plane already knows the file list and sizes, and to have the node pre-create the bundle with the manifest in place. Each upload batch then writes its files straight into the bundle at those offsets with a positioned writer. Batches can arrive in any order and in parallel; they never overlap because the offsets were fixed before the first byte arrived. Finalize is a rename from the staging name to the final name.

What it did on the test node

StepBeforeAfter
Finalize a 420,000-file uploadAbout 5 minutesUnder 10 seconds
Cold-cache archive build for downloadAbout 1,000 files/sAbout 2,800 files/s, reading the bundle sequentially with bounded read-ahead
Per-request session checkLoaded the full path listReads a session summary

The second-order effects were the ones we cared about most. Downloads of file-heavy packages now read one object sequentially instead of seeking through hundreds of thousands of inodes. Storage cleanup deals with one object per package. And the node's filesystem is no longer the thing that decides how large a folder a browser can send.

Related

More from the blog