Resumable uploads

Pause, resume, and survive reloads and crashes.

"use client"

import * as React from "react"
import { MiB, createIndexedDBPersistence, s3Adapter } from "@uploadcn/core"
import { useUploadSelector, useUploader } from "@uploadcn/react"
import { HistoryIcon } from "lucide-react"

import { demoTransport } from "@/examples/_demo"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
  Upload,
  UploadDropzone,
  UploadDropzoneDescription,
  UploadDropzoneTitle,
  UploadQueue,
} from "@/components/ui/upload"

/** Files ≥ 5 MB use multipart, so they resume from the last finished part. */
const adapter = s3Adapter({
  endpoint: "/api/upload",
  transport: demoTransport, // docs only, see examples/_demo.ts
  multipart: { threshold: 5 * MiB, partSize: 5 * MiB },
})

export default function ResumableUploadExample() {
  const [persistence] = React.useState(() =>
    createIndexedDBPersistence({ name: "uploadcn-docs-resumable" })
  )
  const uploader = useUploader({ adapter, persistence, restore: true })
  const restored = useUploadSelector(
    uploader,
    (state) =>
      state.items.filter((item) => item.restored && item.status === "paused")
        .length
  )

  return (
    <Upload uploader={uploader}>
      {restored > 0 ? (
        <Alert>
          <HistoryIcon />
          <AlertTitle>
            {restored} upload{restored === 1 ? "" : "s"} restored
          </AlertTitle>
          <AlertDescription>
            Press resume to continue from the last finished part.
          </AlertDescription>
        </Alert>
      ) : null}
      <UploadDropzone>
        <UploadDropzoneTitle>
          Drop a large file, then reload the page
        </UploadDropzoneTitle>
        <UploadDropzoneDescription>
          The file and its finished parts are kept in IndexedDB
        </UploadDropzoneDescription>
      </UploadDropzone>
      <UploadQueue />
    </Upload>
  )
}

Pause and resume

Pausing aborts the in-flight request. What happens on resume depends on the adapter:

AdapterResumableOn resume
s3Adapter (multipart)✓Continues from the last finished part
multipartAdapter✓Continues from the last finished part
tusAdapter✓Asks the server for the offset and continues
s3Adapter (single PUT), presignedAdapter, httpAdapter✗-

UploadPause only appears for resumable uploads (uploader.canPause(item)), so users are never offered a pause that would silently restart. Queued items can always be paused.

Surviving reloads

Pass a persistence store. Queued and in-progress uploads, including the file bytes, are written to IndexedDB along with each adapter's resume state.

const [persistence] = React.useState(() => createIndexedDBPersistence())
const uploader = useUploader({ adapter, persistence, restore: true })

After a reload, restore brings them back as paused with pauseReason: "restored". Call uploader.resume() (or let users press resume) to continue.

Good to know

  • Storage quotas. Browsers allow IndexedDB to use a large share of free disk space, but may evict it under pressure. Call requestPersistentStorage() to ask for durable storage.
  • Multiple tabs. When Web Locks are available, each running upload holds a lock, and restore() skips uploads another tab is already running.
  • Expiry. Records older than 7 days are dropped (maxAge).
  • Server state. Incomplete S3 multipart uploads are billed until aborted. Add a bucket lifecycle rule to clean up abandoned ones.

Persistence for other stores

Implement the three-method interface to persist elsewhere:

interface UploadPersistence {
  load(): Promise<PersistedUpload[]>
  save(record: PersistedUpload): Promise<void>
  remove(id: string): Promise<void>
}

On this page