---
title: Resumable uploads
description: Pause, resume, and abort a large upload via a control handle, or resume it in a later process from a serializable session token. A per-call option on upload.
---

A long upload over a flaky connection shouldn't have to start over when it's interrupted. Pass an [`UploadControl`](/api/upload) to [`upload`](/api/upload) and you can `pause()` it, `resume()` it, or `abort()` it — and, because the control holds a **serializable session token**, you can persist it and resume the upload in a later process, after a crash, or on the next page load.

```ts lineNumbers
import { Files, UploadControl } from "files-sdk";

const control = new UploadControl();
const result = files.upload("backups/db.tar", file, {
  control,
  multipart: { partSize: 16 * 1024 * 1024 },
  onProgress: ({ loaded, total }) =>
    console.log(total ? `${Math.round((loaded / total) * 100)}%` : `${loaded}`),
});

control.pause(); // in-flight parts settle; `result` stays pending
control.resume(); // pick up where it left off
await result;
```

It's an [`AbortSignal`](/cancellations)-style handle: a plain object you construct and hand in, then drive from the outside. The `upload` call returns its usual promise, which resolves when the upload completes (and stays pending while paused).

## Resuming across processes

`control.toJSON()` returns a small JSON-serializable token describing the provider-side session. Persist it wherever you like — disk, `localStorage`, a database row keyed by the user's upload. Later, rebuild a control from it with `UploadControl.from(token)` and call `upload` again with the **same body**: the SDK discovers what already landed server-side and uploads only the rest.

```ts lineNumbers
// First run — pause and persist.
const control = new UploadControl();
files.upload("backups/db.tar", file, { control }).catch(() => {});
// …once a session exists, control.toJSON() is populated…
localStorage.setItem("upload", JSON.stringify(control.toJSON()));

// Later — a new tab, a new process, after a crash.
const token = JSON.parse(localStorage.getItem("upload")!);
const result = await files.upload("backups/db.tar", file, {
  control: UploadControl.from(token),
});
```

Because resuming re-reads the bytes, `control` requires a body with a **known length** — a `File`, `Blob`, `ArrayBuffer`, typed array, or `string`. A bare `ReadableStream` is rejected: a consumed stream can't be replayed. (Keep the `File` handle around, the way a browser upload widget does.)

## Pause, abort, and the session

- **`pause()`** stops dispatching new parts. Parts already in flight finish, then the upload waits. The session is preserved, so this is the moment to `toJSON()` and persist.
- **`resume()`** continues a paused upload.
- **`abort()`** cancels the upload **and discards the provider-side session** — the partial upload a provider might otherwise bill or retain is cleaned up. It's terminal: the token can no longer be resumed.

Aborting via the [`signal`](/cancellations) option instead cancels the call but _keeps_ the session, so you can resume it later. `control.status` (`"idle"`, `"uploading"`, `"paused"`, `"completed"`, `"aborted"`, `"error"`) and `control.loaded` / `control.total` track progress for a UI.

## What each adapter does

The token maps onto whatever resumable primitive the provider exposes. Most adapters resume **across processes** — persist the token, resume after a crash:

- **S3 and the S3-compatible adapters** (incl. R2 over HTTP) drive S3's native multipart API directly — the token carries the `UploadId`, resume lists already-uploaded parts with `ListParts`, and `abort()` issues `AbortMultipartUpload`. (This is a separate path from the [`multipart`](/multipart) flag's `@aws-sdk/lib-storage` upload, which doesn't expose the upload id.)
- **GCS**, **Firebase Storage**, and **Google Drive** resume against a stored resumable-session URI.
- **Azure Blob** stages blocks and commits them, skipping blocks already staged on resume.
- **OneDrive** drives a Graph upload session and reads `nextExpectedRanges` to resume.
- **Dropbox** drives an upload session, tracking the byte offset in the token.
- **Vercel Blob** drives its native multipart API; the token carries the completed parts (it exposes no server-side list).
- **The local filesystem** writes a `.fls-part` temp file and resumes from its size, renaming it into place on completion.
- **FTP** and **SFTP** resume by querying the remote file's size and appending the rest.
- **Supabase** drives the resumable [TUS](https://tus.io) endpoint; **Appwrite** and **Cloudinary** drive their chunked `Content-Range` uploads.

A few providers expose no serializable session, so they support pause/resume **in-process only** — `control.toJSON()` can't be resumed in a new process: **Box** (its commit requires a whole-file digest), **bun-s3**, and **memory** (no upload id). They buffer the body in the running process and upload on completion.

Every remaining adapter — Netlify Blobs, UploadThing, PocketBase, Bunny, Convex, and the rest — throws a clear "not supported" [`FilesError`](/api/errors) when `control` is passed, the same way an unsupported [range download](/api/download) does. `control` is a single-key option; it isn't available in the [array form](/bulk) of `upload`.

> The Supabase (TUS), Appwrite, and Cloudinary drivers are built to each
> provider's documented chunked-upload protocol and covered by mocked tests,
> but haven't been exercised against a live account — verify them end-to-end
> before relying on them in production.

## Parts, retries, and progress

`partSize` and `concurrency` come from the [`multipart`](/multipart) option and tune the same trade-off. Each part (or chunk) is retried on its own under the call's [retry](/retries) policy, so a transient failure mid-upload doesn't restart the whole transfer. [`onProgress`](/api/onprogress) fires as parts confirm, and on resume it starts from the bytes already uploaded.
