September 17, 2026

Resumable video upload in React Native

Masroor Ahmed
Masroor Ahmed
AI/ML Engineer

Mobile networks aren't bad connections, they're changing connections. A phone hands off between cell towers. It switches from Wi-Fi to cellular when someone leaves the building, and drops the app to the background when a call arrives. A plain fetch of a whole file treats every one of those as a failure and starts again at byte zero. Survivable for a photo. Not for a 900 MB video.

TL;DR

Chunked upload splits the file and resumes from the last confirmed byte. A network handover costs the current chunk instead of the whole transfer. Create the signed URL on your backend and omit X-Client-Type: web-browser. A native uploader PUTs chunks directly, and sending that header fails every chunk with HTTP 400. On the device, pass expo-image-picker's file:// URI through unchanged. Set chunkSize to a multiple of 256 that's at least 5120 KB, because the SDK validates it in the constructor and throws before any bytes are sent. The SDK already resumes on its own; pause() and resume() are for user control on top.

How resumable video upload works in React Native

Chunked upload splits the file into pieces and uploads them one at a time, with the storage layer acknowledging each piece as it lands.

That acknowledgement is what makes resuming possible. When the connection returns, the client asks which byte the server got to and continues from there. A drop near the end of a large file costs the current chunk, not everything already sent.

FailurePlain fetchChunked and resumable
Wi-Fi to cellular handoverRestart from zeroContinues from last confirmed byte
App backgroundedRestart from zeroContinues on return
Signal lost for 30 secondsRestart from zeroContinues from last confirmed byte
User taps pauseNot possiblePauses, resumes on demand

What you need before you start

  • An Expo app and a development build. The Uploads SDK ships native modules, so Expo Go can't run it.
  • A backend you control that creates signed upload URLs.
  • A FastPix account with an Access Token ID and Secret Key from activate your account.

The backend must omit one header for device uploads

The signed URL comes from a server, because the Secret Key can't ship inside an app binary where anyone can extract strings from it.

The call is the standard one with a deliberate omission. A browser uploader performs a resumable-session handshake, so it needs X-Client-Type: web-browser on the URL it will use. The React Native uploader PUTs chunks directly and needs the plain device URL. That header must not be sent:

javascript
// server.js
app.post("/uploads", async (_req, res) => {
  const upload = await fastpix.inputVideo.upload({
    corsOrigin: "*",
    pushMediaSettings: { accessPolicy: "public" },
  });
  // No X-Client-Type header. A device upload needs the plain URL.
  res.json({ uploadUrl: upload.data.url, uploadId: upload.data.uploadId });
});

Sending it anyway produces a failure that points nowhere useful. Every chunk fails with HTTP 400 while the URL itself looks valid. The reverse mistake, omitting it for a browser upload, produces error code 0 after five attempts. Both directions are covered in where the upload URL is created in every framework.

Keep the uploadId. It's how you match the finished video to your own record once encoding completes. The upload options reference covers the rest of the call.

You can run this against a real upload before committing to it. The free plan covers 10 videos and 100K streaming minutes a month with no credit card.

Install the Expo video upload packages and pick a file

Two packages, both installed through expo install so the versions match your SDK:

bash
npx expo install @fastpix/react-native-uploads expo-image-picker

expo-image-picker opens the system library and returns a file:// URI. Pass that URI through unchanged. The SDK normalises the scheme itself, so stripping it first is work you don't need to do.

text
const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: "videos" });
if (result.canceled) return;
const fileUri = result.assets[0].uri;

On Android the picker requests media permission on first use, so there's nothing extra to wire for the happy path.

Which chunkSize values the SDK accepts

endpoint takes an async function rather than a URL string. The SDK calls it as the upload begins. URLs get minted for files actually being uploaded, not for every page view.

text
import { FastPixUpload } from "@fastpix/react-native-uploads";

const upload = new FastPixUpload({
  endpoint: createUploadUrl,
  fileUri,
  chunkSize: 5 * 1024,
});
uploadRef.current = upload;

upload.on("progress", ({ percentage }) => setPercentage(percentage));
upload.on("success", () => setStatus("success"));
upload.on("error", ({ message }) => setError(message));

await upload.start();

chunkSize is in KB, and the SDK checks it in the constructor, so breaking either of the two rules throws before a single byte leaves the device.

The value has to divide by 256, which is the storage layer's rule for multipart parts. Break it and you get chunk size should be divisible by 256. It also has to be at least 5120, which is the SDK's own floor, MIN_CHUNK_SIZE_KB, and that check reports [FastPix] "chunkSize" must be at least 5120 KB (5 MB).

So 5 * 1024 is both the floor and the sensible default. Larger chunks mean fewer round trips and more bytes to re-send when one fails. The failure is loud either way, which is the useful part: a bad value is a thrown error at construction, not a mystery HTTP 400 partway through a long upload.

Keeping a ref to the upload is what makes user controls possible. pause() and resume() are methods on the instance, so the buttons need something to call them on.

Automatic resume and user-driven pause are different

The SDK already resumes on its own when the network drops and returns, and when the device switches between Wi-Fi and cellular. You don't wire that up.

pause() and resume() sit on top of that. They're for the person who wants to stop a large upload after leaving Wi-Fi, because they care about their data plan. Offering that control is worth doing, and it's a product decision rather than a reliability one:

text
function togglePause() {
  const upload = uploadRef.current;
  if (!upload) return;
  if (status === "uploading") {
    upload.pause();
    setStatus("paused");
  } else if (status === "paused") {
    upload.resume();
    setStatus("uploading");
  }
}

Other events worth subscribing to are stateChange, pause and resume. They keep a progress UI honest when recovery happens without a user action.

Knowing when the uploaded video is ready to play

The success event fires when the bytes are delivered. The video still has to be encoded before it can play. On a phone that gap is more visible than on the web, because the person who just uploaded is usually still looking at the screen.

Do not poll from the device. The reliable signal is the video.media.ready webhook to your backend, which carries both the media ID and the playback ID. Your app then learns about it the way it learns about anything else, through your own API or a push.

Polling from a phone costs battery and costs the user's data. It also stops entirely when the app is backgrounded, which is exactly when a long encode finishes. The webhook path is covered in the video.media.ready webhook, and how to verify it.

Which player you hand it to is covered in expo-video vs react-native-video in 2026. Once you have the playback ID, playback is the quickstart player pointed at https://stream.fastpix.com/{PLAYBACK_ID}.m3u8.

Run a resumable upload from a real device

Install @fastpix/react-native-uploads and expo-image-picker, point endpoint at a backend route that omits the browser header, and set chunkSize to 5 * 1024. Then start an upload and turn Wi-Fi off halfway through, because that's the test the feature exists for. The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Enough to prove the resume path before it matters.

Frequently Asked Questions (FAQs)

How do I upload a video from a React Native app?

Create a signed upload URL on a backend you control. Then upload from the device with the FastPix React Native Uploads SDK, which chunks the file and resumes from the last confirmed byte. Pick the file with expo-image-picker and pass its file:// URI through unchanged. The Secret Key never ships in the app, because strings can be extracted from any mobile binary.

Why does my React Native upload fail with HTTP 400 on every chunk?

Because the backend sent X-Client-Type: web-browser when it created the upload URL. That header is for browser uploaders, which perform a resumable-session handshake. The React Native uploader PUTs chunks directly and needs the plain device URL, so the header must be omitted from the server call.

What chunk size should I use for mobile video upload?

chunkSize is in KB. It must divide by 256 and be at least 5120, which makes 5 * 1024, or 5 MB, both the minimum and a good default. The SDK validates the value in the constructor, so a bad one throws chunk size should be divisible by 256 or [FastPix] "chunkSize" must be at least 5120 KB (5 MB). before any bytes are sent. Larger chunks mean fewer round trips and more bytes to re-send when one fails.

Does the upload resume automatically if the network drops?

Yes. The SDK resumes on its own when the connection returns and when the device switches between Wi-Fi and cellular. pause() and resume() exist on top of that for user-initiated control, such as someone stopping a large upload after leaving Wi-Fi.

How do I know when the uploaded video is ready to play?

Listen for the video.media.ready webhook on your backend and tell the app through your own API. It carries both the media ID and the playback ID. Polling from the device is worse on mobile than on the web. It costs battery and data, and it stops when the app is backgrounded, which is often when a long encode finishes.

Share

Stay Ahead of Video
Streaming Trends

Start shipping video today.