A large upload should not require a similarly large JavaScript buffer. This tutorial separates the byte-writing problem from the application protocol: the downloadable helper writes one bounded chunk to a private staging file, counts the bytes actually received, and removes incomplete output.
The helper was tested with Node.js 20.11.1. The Next.js App Router integration below is a boundary sketch, not a complete authenticated upload service. You need a maintained Node release, private writable storage, an authenticated upload-session table and a proxy configured for your request sizes.
Decide the upload contract
Split the browser's file into, for example, 4 MiB chunks. At session creation, record the owner, total size, chunk size, expected chunk count, expiration and quota reservation. The server calculates each chunk's expected length; it never trusts a client-supplied content length as proof.
Use an opaque server-generated identifier for every storage object. Original filenames belong in metadata. A session should move through receiving, assembling and complete states, and only complete files should be downloadable. Chunk requests must verify ownership, state, index and expiry before accepting bytes.
Stream and count
The download contains this small project, without framework dependencies:
stream-large-uploads-in-nextjs/
├── README.md
├── save-chunk.mjs
└── test.mjsThe important operation is a pipeline, not request.arrayBuffer(). The helper inserts a counting transform between the request and destination. It rejects excess bytes immediately, rejects a short body at the end, and creates the destination with exclusive creation so a collision cannot overwrite another file.
// Imports and application services omitted deliberately.
export const runtime = "nodejs";
export async function PUT(request) {
// Must authenticate, enforce CSRF policy and reserve this chunk slot.
const claim = await claimAuthorizedChunk(request);
if (!request.body) return new Response("Missing body", { status: 400 });
// claim supplies a new private staging path and server-derived byte count.
await saveChunk(
request.body, claim.stagingPath, claim.expectedBytes, request.signal
);
// Verify digest, publish immutable chunk and record acknowledgement.
await finishChunkClaim(claim);
return new Response(null, { status: 204 });
}The unimplemented application services are essential: they need database-backed claims or equivalent coordination, quotas and recovery after crashes. Do not replace them with a client filename or an in-memory boolean. Two server workers must not simultaneously publish different data under the same chunk index. Define retries as accepting the same chunk digest, but rejecting a conflicting replacement.
Next.js route handlers expose standard request bodies; use the Node runtime for filesystem work. Node's pipeline supports error propagation and cancellation. These are the specific APIs used here, not evidence that every hosting provider supports unlimited upload duration. Next.js route handlers, Node streams.
Keep the entire path bounded
Streaming application code does not override a CDN's upload cap, Nginx buffering, function timeouts or available disk space. Check each layer. A 4 MiB chunk should fit comfortably below the smallest request limit, including any protocol overhead.
Limit concurrent chunk requests per account and globally. Streaming has buffers, so “not the whole file” does not mean zero memory. Keep assembly streaming too, or use object-storage multipart completion. Never acknowledge a chunk merely because the browser finished sending it; acknowledge after the chosen durability boundary.
Test partial failures
Run the downloaded tests:
node --test test.mjsThey cover exact length, oversized and incomplete bodies, cancellation, partial-file cleanup and preserving a pre-existing destination. They do not exercise a running Next.js deployment.
Before release, disconnect during a chunk, kill the worker after the file write but before database acknowledgement, retry an existing chunk, exhaust a test quota and upload through the actual proxy. Measure process memory with several slow uploads. Expect orphan staging files after a hard crash; a separate reconciler must remove only abandoned claims.
If uploads fail at the same size, inspect proxy limits first. If memory grows with the full file size, search for buffering in parsers and assembly. For the next protocol layer, see resumable upload progress. CodeKits can help integrate this into a Next.js client portal.
