Documentation menu

Resumable Uploads

upup supports two resumable protocols. Both are opt-in through the single resumable prop, and they are mutually exclusive — a resumable value is either a multipart config or a tus config, never both.

Multiparttus
Upload targetserverUrl (@upupjs/server)tus endpoint
Splits large files into partsYesYes (when chunkSizeBytes is set)
Survives a page reloadNoYes (fingerprint URL storage)
Extra dependencyNonetus-js-client (optional peer)
StorageYour S3-compatible bucketWhatever the tus server writes to

Pick multipart if your bytes already go to S3 through @upupjs/server and you mainly want large files split into parallel, individually-retried parts. Pick tus if you run (or rent) a tus server and need an upload to survive the browser tab closing.

Multipart

Multipart splits a file into parts, signs each part individually, uploads three parts at a time, and then asks the server to assemble them. Core routes a file to the multipart strategy when its size is at or above thresholdBytes; everything smaller takes the ordinary single-PUT presigned path.

tsx
<UpupUploader
    mode="server"
    serverUrl="/api/upup"
    resumable={{
        protocol: 'multipart',
        thresholdBytes: 5 * 1024 * 1024,
        chunkSizeBytes: 8 * 1024 * 1024,
    }}
/>

Multipart requires server mode

Multipart needs a credential source that can init, sign parts, complete, and abort. Only serverUrl (backed by @upupjs/server) provides those. A client-mode uploadEndpoint supplies presigned single-PUT URLs only, so pairing it with protocol: 'multipart' throws a UpupConfigError ("CredentialStrategy must implement multipart methods") the moment the uploader is constructed.

Options

Three fields, all optional.

OptionTypeDefaultWhat it does
thresholdBytesnumber5242880 (5 MiB)Files at or above this size use multipart; smaller files use a single presigned PUT.
chunkSizeBytesnumber5242880 (5 MiB)Requested part size. A fallback only — see Part size, the server decides the real value.
persistbooleanCurrently ignored — reserved. No upload path reads it in 3.1.0, so setting it has no effect either way.

Part concurrency is fixed at three parts in flight and is not configurable through resumable.

Server routes

Multipart is a four-call lifecycle. All four are POST, all four are implemented by @upupjs/server under your serverUrl base path:

txt
POST /multipart/init
POST /multipart/sign-part
POST /multipart/complete
POST /multipart/abort

/multipart/init returns { key, uploadId, partSize, expiresIn, token }. The token is an HMAC-signed capability bound to the key, upload id, and a byte envelope; the client replays it on every subsequent call, and an init response without one is rejected client-side as a misconfigured or outdated server. If any part fails, core issues a best-effort /multipart/abort so no orphaned multipart upload is left billing you for storage.

Request and response shapes for all four routes are documented in the server HTTP reference.

Part size, the 5 MiB floor, and the 10,000-part cap

S3 imposes two hard limits that shape every multipart upload: a part must be at least 5 MiB (except the last one), and a single upload may have at most 10,000 parts. @upupjs/server reconciles both before answering /multipart/init:

  1. Start from the requested chunk size, or 5 MiB if none was given.
  2. Raise it to 5 MiB if it is below the floor.
  3. Raise it again to ceil(fileSize / 10000) if the file would otherwise need more than 10,000 parts.

The returned partSize wins — core uses the server's value and falls back to your chunkSizeBytes only if the server omits it. In practice that means the part size in server mode is decided by the server, not by the browser.

The auto-bump in step 3 is why there is no hard file-size ceiling: a part size of P covers files up to P × 10000, and anything larger simply gets a bigger part size.

Part sizeFiles covered without an auto-bump
5 MiB (the floor)up to ~48.8 GiB
8 MiBup to ~78.1 GiB
16 MiBup to ~156.3 GiB
64 MiBup to ~625 GiB

Larger parts mean fewer signing round-trips but coarser progress and more bytes to re-send when one part fails. The 5 MiB default is the right choice unless you routinely move files in the tens of gigabytes.

What multipart recovers, and what it doesn't

Multipart parallelizes and isolates failures — a failed part is retried on its own rather than taking the whole file down with it. Recovery beyond that is within a single page session only:

  • pause() aborts the in-flight requests. resume() and retry() then re-run every file that has no storage key yet, starting from part one — the parts already accepted by S3 are not reused.
  • Cross-reload resume is not wired. A page reload discards the in-memory upload, and nothing re-attaches to the server-side upload id.

A localStorage-backed session store exists in the codebase for building cross-reload resume yourself. It is exported from @upupjs/core/internal (fileFingerprint, saveSession, loadSession, updateSessionProgress, removeSession, clearAllSessions), keys entries under the upup_mp_ prefix, expires them after 24 hours, and returns null rather than throwing when an entry is corrupt or localStorage is unavailable (private mode, quota). No upload path calls it for you — it is a building block you would have to wire up, not behavior you get by turning multipart on.

If you need an upload to survive a closed tab out of the box, use tus.

tus

tus talks directly to an external tus-compatible service — your own tusd, Transloadit, or any other implementation of the protocol. @upupjs/server is not involved.

tus-js-client is an optional peer dependency, kept out of core's mandatory path so projects that never use tus don't pay for it. Install it yourself:

bash
npm i tus-js-client

The error you get without it

The import is dynamic, so a missing tus-js-client fails at upload time rather than at construction time: the first tus upload rejects with a UpupError carrying code UPLOAD_FAILED and the message "Resumable (tus) uploads require the optional dependency 'tus-js-client'. Install it: npm i tus-js-client".

tsx
<UpupUploader
    resumable={{
        protocol: 'tus',
        endpoint: 'https://uploads.example.com/files',
        chunkSizeBytes: 8 * 1024 * 1024,
        retryDelays: [0, 1000, 3000, 5000],
        headers: { Authorization: `Bearer ${token}` },
        metadata: { album: 'summer-2026' },
    }}
/>

Options

Nine fields. endpoint is required; every other field is passed through to tus-js-client only when you set it, so an omitted field keeps that library's own default.

OptionTypeDefaultWhat it does
endpointstringRequired. The tus creation endpoint. Also counts as your upload target.
chunkSizeBytesnumberInfinity (one request)Bytes per PATCH request. Set it to get real chunking — and resumability that survives a mid-upload failure.
chunkSizenumberDeprecated alias of chunkSizeBytes, still honored. chunkSizeBytes wins when both are set.
retryDelaysnumber[][0, 1000, 3000, 5000]Backoff schedule, in milliseconds, for automatic retries. Pass [] to disable retrying.
storeFingerprintForResumingbooleantrueStore the upload URL in localStorage, keyed by a file fingerprint, so a later upload of the same file resumes instead of restarting.
removeFingerprintOnSuccessbooleanfalseDelete that stored entry once the upload succeeds. Turn it on if users re-upload the same filename often.
headersRecord<string, string>Extra headers on every tus request — the usual place for an auth token.
metadataRecord<string, string>Upload-Metadata entries. Merged over the filename and filetype upup sets, so a key of either name overrides upup's value.
parallelUploadsnumber1Split the file into N partial uploads sent concurrently, then concatenate server-side. Requires the tus Concatenation extension.

A completed tus upload reports the tus upload URL as the file's storage key.

parallelUploads and resuming interact

tus-js-client stores fingerprints for whole uploads, not for the partial uploads created by parallelUploads. With parallelUploads above 1 the partial uploads are not fingerprinted, so a fresh page load restarts them. Leave it at 1 if resume-after-reload matters more than raw throughput.

Choosing exactly one upload target

uploadEndpoint, serverUrl, and a tus endpoint are three upload targets and you may configure exactly one. Two or more throws a UpupConfigError with code AMBIGUOUS_UPLOAD_TARGET at construction time.

When a tus config is present it takes precedence for every file regardless of size — thresholdBytes is a multipart-only concept and has no tus equivalent.

Crash recovery is a different feature

Do not confuse resumable uploads with crash recovery. They solve neighboring problems and can be enabled independently:

  • Resumable uploads are about the bytes of one in-flight transfer — parts, retries, and (for tus) reattaching to a partly-uploaded object on the server.
  • Crash recovery (crashRecovery) is about the uploader's own state. It writes a snapshot to IndexedDB holding the file list — each file's blob plus its id, name, type, source, status, metadata, and storage key if it got one — along with the overall uploader status, and restores that after an unexpected unmount or reload.

The snapshot carries no partial upload progress: no byte offsets, no part list, no multipart upload id. Crash recovery gives the user their file list back; it does not pick a transfer up mid-stream. Conversely, enabling multipart does not persist selections. See the reliability guide for how the two combine and which one to reach for.