Repository navigation
Stream large files to S3 with multipart uploads - #12070
Merged
Merged
Conversation
Contributor
Preview Build AvailableA preview build has been created for this PR. You can install it using: pnpm install "remix-run/remix#preview/pr-12070&path:packages/remix"This preview build will be updated automatically as you push new commits. |
`storage.set()` read the whole file into memory with `arrayBuffer()` before sending a single PUT, so streaming uploads from `form-data-parser/future` were buffered again on their way to S3. Files are now read from `file.stream()` in fixed-size parts. Files that fit in one part still use a single PUT; larger files use an S3 multipart upload that holds at most two parts in memory and is aborted on failure. A new `partSize` option sets the part size (default 8 MiB, minimum 5 MiB).
Node.js before 24.20.0 and 26.6.0 retains every chunk read through Blob#stream(), so streaming native File objects to S3 leaked each stored file. Blobs are now read in parts with slice().arrayBuffer(), and only streaming-only inputs such as the future FileUpload use stream().
Document and test streaming sources generically, since this change no longer depends on form-data-parser/future, and note when the Blob slice workaround can be removed.
mjackson
force-pushed
the
mjackson/stream-s3-uploads
branch
from
October 23, 2026 17:11
7dcf54f to
3b2fb70
Compare
mjackson
changed the base branch from
mjackson/streaming-file-uploads
to
main
October 23, 2026 17:11
Contributor
|
The preview branch |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
storage.set()in the S3 backend reads the whole file witharrayBuffer()before it sends a singlePUT, so storing a 500 MB file needs 500 MB of RAM even when the file is on disk or is being streamed from somewhere else. This PR streams files to S3 instead.set()reads the file in fixed-size parts.Blobs andFiles are read withslice().arrayBuffer().Blob#stream()on Node.js before 24.20.0 and 26.6.0 keeps every chunk in memory for the life of the process (nodejs/node#63574), so it would leak every stored file. A comment notes that this workaround can be removed once the minimum supported Node.js version is >=24.20.0.File-shaped inputs are read withstream(). Onlystream(),name,typeandlastModifiedare used, so this also works with streaming sources whosesizeisn't known until they've been read.PUT.partSizebytes, which providers like Cloudflare R2 require.partSizeoption: defaults to 8 MiB, which supports files up to about 78 GiB, and must be at least 5 MiB, the S3 minimum.put()uses the same upload path. It still holds the whole file in memory because it returns aFilewith the stored contents, and the docs point large files toset().Peak memory growth when storing one file read from a stream with
set(), using a fetch that discards request bodies (Node.js 24.18):Before, memory grows with the file. After, it stays flat, and what's left is garbage-collection lag on discarded part copies. Storing ten 20 MiB in-memory
Files leaves no memory retained after garbage collection.The unit tests cover part splitting, reading
Blobs withoutstream(), the single-PUTcutoff, how far reading runs ahead of uploading, abort on part and stream failures, and failed completions. A new integration test runs a real multipart upload against LocalStack in CI.This is independent of #12029 and #12030. Once #12030 lands, streaming uploads from
remix/form-data-parser/futurecan go straight from the request to S3 throughset().