Uppy 6.0 and Resumable File Uploads
Set up resumable file uploads with Uppy and Tus, explore Uppy 6.0 changes to S3 signing and core packages, and check key migration steps from version 5.
Uppy is an open-source JavaScript file uploader. Paired with its @uppy/tus plugin, it resumes large uploads after a dropped connection or a closed tab, so the user doesn’t have to send the file again from the start.
Most teams reach for a library after the same bug report: someone on hotel Wi-Fi reached 90% of a 2 GB video, the connection dropped, and the upload went back to zero. If you built your own uploader from our guides on building a file upload dropzone or custom file upload in React, this is the point where that approach stops being enough.
This article covers why resumable uploads matter, a minimal Uppy setup, what changed in Uppy 6.0, and how much work the upgrade from 5.x takes.
Key Takeaways
- Tus is an open protocol, built on HTTP, that makes uploads resumable. A dropped connection or a closed tab doesn’t lose progress: the upload carries on from the last byte the server received.
- Uppy 6.0 rewrites
@uppy/aws-s3around three signing modes (getCredentials,signRequestandcompanionEndpoint), and each configuration uses exactly one of them. - In Uppy 6.0,
@uppy/utils,@uppy/store-default,@uppy/companion-clientand@uppy/provider-viewsnow ship inside@uppy/coreas subpath exports. - Projects that use the
uppymeta-package or the CDN bundle need few or no code changes to move to 6.0. - Self-hosted Companion must be upgraded to Companion 7 before the client moves to
@uppy/core6, because OAuth tokens now travel over Companion’s WebSocket. Companion 7 also runs on Express 5.
Why Use Uppy Instead of a Hand-Rolled Uploader?
A hand-rolled uploader built on a single fetch or XMLHttpRequest POST can’t pick up where it left off. When the connection drops, all the bytes already sent are lost. Tus solves this at the protocol level: the client asks the server how much it already has, then sends only the rest. Session replay makes the cost of not having this easy to see: after a failed upload, you can watch the user go back to the file picker and choose the same large file again.
Tus resumable uploads do need a server-side counterpart. The @uppy/tus docs recommend tusd, and Tus servers can store files locally or in external storage such as S3.
Minimal Uppy Setup with Dashboard and Tus
Three packages give you a complete resumable uploader: @uppy/core holds the state, @uppy/dashboard provides the UI, and @uppy/tus handles the transfer.
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Tus from '@uppy/tus';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';
new Uppy()
.use(Dashboard, { inline: true, target: 'body' })
// Point this at your own tus server, e.g. a tusd instance
.use(Tus, { endpoint: 'https://your-tus-server/files/' });
inline: true renders the Dashboard straight into the target element instead of opening it as a modal. If you would rather build your own progress UI, our article on building an upload progress bar covers the underlying events.
Remote sources such as Google Drive go through Companion, Uppy’s server component. Companion fetches the file on the server, so the bytes never pass through the user’s browser. One detail matters here: the chunkSize setting in @uppy/tus only covers files sent from the user’s device. Uploads that go through Companion follow Companion’s own chunkSize setting, so set both if you need a size limit on each request.
By default, @uppy/tus retries a failed chunk after a series of delays and limits how many uploads run at the same time. When the server returns HTTP 429, its default onShouldRetry logic pauses all queued requests and waits before trying again, stepping through the delays in retryDelays.
What’s New in Uppy 6.0
Uppy 6.0 has three headline changes: @uppy/aws-s3 is rewritten from scratch, four support packages are merged into @uppy/core, and @uppy/instagram is removed. According to the Uppy 6.0 release post, six packages received a major version bump: @uppy/core, @uppy/companion, @uppy/aws-s3, @uppy/tus, @uppy/components and uppy.
In Uppy 5, the old @uppy/aws-s3 plugin had up to eight callbacks, and the set you needed changed with your setup: multipart or single upload, with Companion or without, signing in the browser or on a server. The rewrite replaces all of that with three signing modes, and you pick exactly one:
| Mode | Where signing happens | Companion needed |
|---|---|---|
getCredentials | In the browser (SigV4), using temporary credentials from your endpoint | No |
signRequest | Your own signer | No |
companionEndpoint | Companion | Yes |
import Uppy from '@uppy/core';
import AwsS3 from '@uppy/aws-s3';
new Uppy().use(AwsS3, {
// Swap this for an R2, MinIO or Spaces endpoint
s3Endpoint: 'https://my-bucket.s3.eu-west-1.amazonaws.com',
region: 'eu-west-1',
getCredentials: async () => fetch('/s3-credentials').then((r) => r.json()),
});
Companion is no longer required to upload to S3. With getCredentials, the browser signs each request itself, and the plugin works with any S3-compatible service, including Cloudflare R2, MinIO and DigitalOcean Spaces. The @uppy/aws-s3 reference lists the fields getCredentials must return, so in the example above your /s3-credentials endpoint has to send back that exact shape. The release post also says the rewrite closed long-standing bugs in retry backoff, credential expiry, offline detection and pausing or resuming multipart uploads. The plugin’s CI tests now run against a real MinIO server rather than mocks.
How Do You Upgrade from Uppy 5 to 6?
If you use the uppy meta-package or the CDN bundle, the 6.0 upgrade needs few or no code changes. Everyone else should work through this list, and the official migration guide has the full details:
- Individual packages: move imports from
@uppy/utils,@uppy/store-default,@uppy/companion-clientand@uppy/provider-viewsto their@uppy/coresubpaths. The old packages are deprecated on npm. - Self-hosted Companion: upgrade to Companion 7 first, then move the client to
@uppy/core6. Companion 7 still supports older Uppy clients, but an Uppy 6 client won’t work with Companion 6 or older. Companion 7 is built on Express 5 and won’t work as middleware in an Express 4 app, so move your app to Express 5 before you switch.companion.socket()now needscompanionOptionsas its second argument. Hosted Companion users don’t need to do anything. - Tus error handlers: check your
upload-errorcode again, becauseresponseis now populated.
- import { fetcher } from '@uppy/utils'
+ import { fetcher } from '@uppy/core/utils'
- import { RequestClient } from '@uppy/companion-client'
+ import { RequestClient } from '@uppy/core/companion-client'
CompanionClientProvider and CompanionClientSearchProvider are also gone. Import Provider from @uppy/core/companion-client in their place. The merge exists to prevent a specific failure: a lockfile holding an old copy of @uppy/utils next to a newer @uppy/core, which left two copies of the same package in one dependency tree.
Since Uppy 6.0, @uppy/tus passes the server’s status and response body to the upload-error event and to file.response. Earlier versions emitted the error without the server’s response.
uppy.on('upload-error', (file, error, response) => {
if (error.isNetworkError) return showOfflineNotice(); // placeholder helper
console.log(response?.status);
console.log(response?.body.xhr.responseText);
});
You still get undefined for response in two cases: the server never replied (such as when it can’t be reached), or the file was a remote one uploaded through Companion.
Using Uppy with React and Other Frameworks
Uppy’s framework wrappers let components subscribe to Uppy state without wiring up events by hand. In React, the useUppyState hook from @uppy/react takes an Uppy instance and a selector, and re-renders the component when the selected slice changes. The React integration docs describe the full API.
import { useState } from 'react';
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
import { useUppyState } from '@uppy/react';
function UploadStatus() {
const [uppy] = useState(() =>
new Uppy().use(Tus, { endpoint: 'https://your-tus-server/files/' }),
);
const files = useUppyState(uppy, (state) => state.files);
const progress = useUppyState(uppy, (state) => state.totalProgress);
return <p>{Object.keys(files).length} files, {progress}% uploaded</p>;
}
The lazy useState initialiser creates the Uppy instance once per component instead of on every render. Uppy also has wrappers for Vue, Svelte and Angular.
When Not to Use Uppy
A single small file, such as a profile avatar, doesn’t need Uppy or Tus. A plain form POST to your own endpoint is enough, and Tus would add a server integration you then have to run. If you later want Uppy’s UI but keep a regular HTTP endpoint, @uppy/xhr-upload covers that: it can send a standard multipart form upload or a PUT request. Our guide to Multer file upload in Node shows the receiving side.
Conclusion
Resumable uploads are why Uppy is worth adding. Uppy 6.0 makes it simpler to run, with one S3 plugin that has a single signing mode per configuration and one core package instead of five that had to stay in step. To start, install @uppy/core, @uppy/dashboard and @uppy/tus, point the Tus plugin at a tusd instance, and upload a large file with your network throttled. If you’re already on 5.x, begin with the Companion and upload-error changes, since those are the ones that can break existing code.
FAQs
What does the getCredentials function in @uppy/aws-s3 need to return?
It returns an object with a credentials property holding temporary credentials as camelCase accessKeyId, secretAccessKey and sessionToken, plus an optional expiration, and a region to sign for. AWS STS returns these keys in PascalCase, so map them first. Uppy caches the result for the plugin's lifetime. On an ExpiredToken or InvalidAccessKeyId error it fetches new credentials and retries once. Scope the attached policy tightly, because the browser holds these credentials.
Which @uppy/aws-s3 options were removed in Uppy 6.0, and does shouldUseMultipart still work?
shouldUseMultipart still works in @uppy/aws-s3 6.0, along with getChunkSize, allowedMetaFields and limit. The removed options are endpoint, headers, cookiesRule, getTemporarySecurityCredentials, getUploadParameters, signPart, createMultipartUpload, listParts, abortMultipartUpload, completeMultipartUpload, uploadPartBytes and retryDelays. Existing Companion setups migrate by renaming endpoint to companionEndpoint. headers and cookiesRule have no direct replacement: same-origin cookies still reach Companion, but bearer tokens or cross-site cookies need another setup, such as an authenticating proxy in front of Companion.
What is the difference between Tus resumable uploads and @uppy/golden-retriever?
Tus lets the server keep the bytes it already received so the transfer continues, while Golden Retriever restores the browser-side file selection and Uppy state after a crash or closed tab. In Uppy 6.0, Golden Retriever stores metadata, state and local files up to 10 MiB in IndexedDB by default, falling back to localStorage for metadata. Larger files need the optional Service Worker store, which survives closed tabs but not browser restarts.
Can an Uppy 6 client connect to an older Companion server?
No. An Uppy 6 client needs Companion 7, because OAuth tokens now travel over Companion's WebSocket connection instead of window.opener. Companion 7 keeps postMessage as a fallback for Uppy clients older than 6.0, so deploy Companion 7 first, then upgrade the client. On the server, pass companion.socket() the same options object you pass to companion.app(). Hosted Companion users need no changes.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k