Tutorial

How to Chunk an Upload for Resume

Split a large file into chunks and upload with offsets, so an interrupted transfer resumes from the last acknowledged chunk instead of restarting.

On this page

  1. Introduction
  2. Choose a chunk size
  3. Slice the file
  4. Upload with offset
  5. Track acknowledged chunks
  6. Resume on reconnect
  7. Test interruption
  8. Common pitfalls
  9. Conclusion

Introduction

The hardest part of resume interrupted uploads is not writing the code — it is deciding what the code is allowed to reject. A chunked upload setup that accepts everything is easy to ship and expensive to operate, because every malformed input becomes a production surprise. The discipline is to enumerate the shapes the system actually receives, encode them in chunked upload, and let the library reject the rest loudly. This keeps the happy path fast and the failure path visible.

A chunked upload guard for resume interrupted uploads should be proportional to the risk. A ten-field form does not need a hundred-line schema; it needs the five checks that catch the five mistakes the users actually make. Ship the five, measure the rejections, and add the sixth only when the traffic proves it exists. Speculative generality is the bug you write on purpose, and chunked upload makes it easy to write, which is why discipline matters.

The sections below walk through the concrete decisions: where to put the chunked upload check, how to keep it fast under load, what to do when the contract changes, and how to test the failure paths so they degrade on purpose instead of by accident.

Choose a chunk size

Choose a chunk size is the first decision because it determines what the rest of the chunked upload pipeline can assume. A wrong choice here turns into resume interrupted uploads under load, and the invariant you thought chunked upload guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Choose a chunk size is the one most teams skip: drive chunked upload to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

async function uploadChunked(file, url) {
  const SIZE = 5 * 1024 * 1024;
  for (let off = 0; off < file.size; off += SIZE) {
    const chunk = file.slice(off, off + SIZE);
    await fetch(url, { method: 'POST', headers: { 'X-Offset': off }, body: chunk });
  }
}

The javascript above is the shape to ship for resume interrupted uploads. It is small enough to audit and specific enough that the failure behavior is obvious. Adapt the constants to your traffic and latency budget, and resist the urge to handle cases the system has not seen yet.

Slice the file

The mechanism behind Slice the file is straightforward in chunked upload, but the consequences ripple. A wrong choice here turns into resume interrupted uploads under load, and the invariant you thought chunked upload guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Slice the file is the one most teams skip: drive chunked upload to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Upload with offset

Upload with offset is where most chunked upload implementations leak performance. A wrong choice here turns into resume interrupted uploads under load, and the invariant you thought chunked upload guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Upload with offset is the one most teams skip: drive chunked upload to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

test('sends correct offsets', async () => {
  await uploadChunked(file, url);
  expect(fetch).toHaveBeenCalledWith(url, expect.objectContaining({ headers: { 'X-Offset': 0 } }));
});

Pin the behavior with this test. A chunked upload setup that passes this test degrades on purpose instead of by accident, which is the difference between a managed failure and an incident.

Track acknowledged chunks

The hard part of Track acknowledged chunks is not the happy path — it is what happens when chunked upload fails halfway. A wrong choice here turns into resume interrupted uploads under load, and the invariant you thought chunked upload guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Track acknowledged chunks is the one most teams skip: drive chunked upload to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Resume on reconnect

Resume on reconnect should be observable without a debugger. A wrong choice here turns into resume interrupted uploads under load, and the invariant you thought chunked upload guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Resume on reconnect is the one most teams skip: drive chunked upload to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Test interruption

Test interruption is a trade, not a winner. A wrong choice here turns into resume interrupted uploads under load, and the invariant you thought chunked upload guaranteed no longer holds. Measure the impact before committing, because the cost of a bad choice is paid in incidents, not in a code review.

The test for Test interruption is the one most teams skip: drive chunked upload to the failing condition and assert the system degrades gracefully. Write it with a fake clock for time-based logic, a stub for the failure path, and an assertion on the user-visible outcome. A test that passes by timing out is a hope, not a test.

Common pitfalls

The common pitfalls in resume interrupted uploads all reduce to one mistake: treating chunked upload as a formality instead of a contract. The contract is the thing the rest of the system relies on, and a weak contract propagates weakness everywhere.

  • Skipping the chunked upload boundary so resume interrupted uploads enters unvalidated.
  • Assuming chunked upload is fast without measuring the tail latency under load.
  • Letting resume interrupted uploads fail silently instead of failing loudly with a metric.
  • Testing only the happy path so the chunked upload failure surfaces in production.
  • Sharing mutable state across requests so one bad input poisons the next.

Conclusion

Do not conflate the conclusion with the implementation. They look similar from the outside but the failure modes differ: chunked upload fails fast and the caller knows, while an unchecked path fails slow and the caller guesses. Choose the one whose failure mode your system can absorb, and document the choice at the call site.

Self-check: write a test that drives chunked upload to the failing condition and assert the system degrades gracefully. Then resume interrupted uploads is a property you maintain, not a feature you hope for.

Sources: tus protocol, Resumable upload

FAQ

What chunk size?
5–10MB for most files. Smaller for flaky networks, larger for throughput.
How track progress?
Sum acknowledged chunk sizes. Show percent of total file size.

Related Examples