Blog
Engineering 5 min read

Resumable video uploads with the tus protocol

How the tus protocol makes large video uploads resume instead of restart after a dropped connection, with a client example and direct-to-storage flow.

rehelios

Engineering

tus is an open, HTTP-based protocol for resumable file uploads: instead of sending a file as one request, the client uploads it in offset-tracked chunks and can resume from the last byte the server actually received. For large video, that turns a dropped wifi connection or a laptop lid closing mid-upload from a full restart into a brief pause. Done right, the upload also goes straight from the browser to storage — your API server issues a URL and never touches the video bytes.

Why naive uploads fail for big video

A standard multipart/form-data POST works fine for a 2MB avatar. It falls apart for a 4GB screen recording or a raw camera export, for reasons that compound:

  • Timeouts. Most reverse proxies and serverless runtimes cap request duration between 30 seconds and a few minutes. A multi-gigabyte upload on a real connection routinely exceeds that, and the whole request dies with no way to recover progress.
  • Memory pressure. A naive handler buffers the incoming body before writing it anywhere. A handful of concurrent 3GB uploads turns a small API instance into a memory problem, independent of how good the code is.
  • No resume. If the connection drops at byte 3.9GB of 4GB, the browser has no protocol-level way to tell the server “continue from here.” The only option is starting over, which on a spotty connection can mean an upload that never finishes.
  • Your server sits in the hot path. Every byte flows client → your API → storage, doubling bandwidth cost and making your API’s uptime a hard dependency for what’s fundamentally a client-to-storage transfer.

None of this is a framework problem. It’s what happens when a request/response primitive built for form submissions is used to move gigabytes of binary data across an unreliable network.

How tus works

tus (from the tus.io open protocol) fixes this by breaking the upload into three explicit steps, each with a defined HTTP method and header contract.

1. Creation. The client sends a POST to the upload endpoint with an Upload-Length header stating the total size in bytes, plus optional Upload-Metadata for things like filename or content type. The server responds 201 Created with a Location header — that URL is the upload’s identity for the rest of the transfer.

2. Chunked PATCH uploads. The client sends the file in pieces via PATCH requests to that Location URL. Each request carries Content-Type: application/offset+octet-stream, an Upload-Offset header stating where this chunk starts, and the raw bytes as the body. The server checks Upload-Offset against what it has actually persisted — a mismatch gets 409 Conflict rather than a silently corrupted file. On success it returns 204 No Content with the updated Upload-Offset.

3. Resume via HEAD. This is what makes tus resumable rather than just chunked. If the connection drops — wifi flakes, the tab closes, the laptop sleeps — the client doesn’t guess where it left off. It sends a HEAD request to the upload URL, and the server responds 200 OK with the current Upload-Offset and Upload-Length. The client resumes PATCHing from exactly that byte. No re-upload, no guesswork.

That’s the core protocol: three verbs, a handful of headers, a server that persists an offset. Checksums, expiration, and parallel concatenation are optional extensions on top.

A minimal client example

You don’t hand-roll the offset bookkeeping yourself — tus-js-client implements the protocol and handles retries and resume detection for you:

import * as tus from "tus-js-client"

const upload = new tus.Upload(file, {
  endpoint: "https://api.rehelios.com/v1/uploads",
  retryDelays: [0, 1000, 3000, 5000],
  metadata: {
    filename: file.name,
    filetype: file.type,
  },
  onError: (error) => console.error("upload failed", error),
  onProgress: (bytesUploaded, bytesTotal) => {
    console.log(`${((bytesUploaded / bytesTotal) * 100).toFixed(1)}%`)
  },
  onSuccess: () => console.log("done:", upload.url),
})

const previousUploads = await upload.findPreviousUploads()
if (previousUploads.length > 0) {
  upload.resumeFromPreviousUpload(previousUploads[0])
}

upload.start()

findPreviousUploads() checks what the browser has recorded locally against a HEAD request to the server, and decides whether to continue an in-progress upload instead of starting fresh — the resumability from the section above, wired into a few lines of client code.

Direct-to-storage uploads

The other half of making this fast and cheap is keeping your application server out of the data path. The pattern:

  1. Your API authenticates the request and decides the upload is allowed (right user, right project, under quota).
  2. It returns a tus-compatible upload URL, or short-lived credentials, pointing at storage — not at your API.
  3. The browser runs the Creation → PATCHHEAD cycle directly against that storage endpoint.

Your server issues the ticket and never sees a video byte. No request-timeout risk, no memory spent buffering uploads, and bandwidth cost that scales with your storage provider’s pricing instead of your compute bill.

What a platform handles for you

Implementing the tus server side correctly — offset tracking, concurrent chunk validation, Upload-Metadata parsing, and wiring temporary storage credentials per upload — is real work before a single frame gets encoded.

rehelios exposes tus-compatible resumable uploads as one endpoint in the REST API: request an upload, get back a tus URL, and the browser uploads straight to storage with any tus client. There’s no tus server to run and no storage credentials to generate yourself. Once the upload lands it’s picked up for encoding automatically — HLS and DASH renditions, ready to play behind signed URLs. If the source video already lives somewhere else, import-by-URL skips the upload step entirely.

For the endpoint details and metadata fields, see the docs. If you’re wiring uploads into an agent or automated pipeline, the MCP server and CLI expose the same flow without writing HTTP calls by hand. And for what happens to the file after it lands, the video CDN primer covers delivery.

Put your first video live today

Create an account, upload a file, and have a fast, embeddable video live in minutes. Pay only for what you store and stream.