Skip to navigation

Set up resumable uploads for React Native

Add resumable, chunked video upload to a React Native app with the FastPix React Native Uploads SDK, so large files survive network drops on Android and iOS.

The FastPix React Native Uploads SDK adds resumable, chunked uploads to your React Native app. It uploads large videos in chunks, retries failed chunks on its own, and resumes from the last confirmed byte after a pause or a dropped connection, so an interrupted upload continues instead of starting over. It works on both Android and iOS.

Uploads go straight from the device to FastPix, so the video bytes never pass through your own server.



A resumable upload splits a large file into smaller pieces, called chunks, and uploads them one at a time:

  1. Split the file. The file is divided into smaller chunks (5 MB by default).
  2. Upload each chunk. Each chunk is sent separately. The server confirms each one.
  3. Resume from the last chunk. If the upload is interrupted, it continues from the last confirmed chunk instead of starting over.

This matters because:

  • It lowers the risk. If a chunk fails on a flaky network, only that chunk is retried, not the whole file.
  • It performs better. Smaller chunks transfer more reliably on slow or mobile connections.
  • It is easier to manage. Chunk-by-chunk progress makes features like pause, resume, and progress bars straightforward.


Platform support

PlatformMinimum version
AndroidAPI 21 (Android 5.0)
iOSiOS 13.0
React Native0.70+

Install the SDK

Install the package with npm or your preferred package manager:

npm install @fastpix/react-native-uploads

The SDK bundles its runtime dependencies (@react-native-community/netinfo, react-native-blob-util, and axios), so there are no peer dependencies to install manually.


iOS: the native modules auto-link on React Native 0.70+. After installing the package, install the pods:

cd ios && pod install && cd ..

Android: no manual linking or extra setup is required. The native modules auto-link.

Note: The SDK depends on native modules, so it does not run in Expo Go. In an Expo project, use a development build (npx expo run:ios or npx expo run:android). For a full Expo walkthrough, see Upload videos from a React Native app.


Generate a signed upload URL

The SDK uploads to a signed URL: a pre-authorized, short-lived URL that lets the device upload directly to FastPix without exposing your API credentials in the app.

To create one, you need a valid Access Token and Secret Key. See the Basic Authentication guide for how to retrieve them. Then call the Upload media from device API to get the signed URL.

Never ship your Access Token or Secret Key in the app bundle. Create signed URLs on your backend and return only the signed URL to the device.

The service below calls the Direct Upload API and returns a signed URL. This is your backend code, not part of the SDK. It uses axios and base-64 for the Basic Auth header (npm install axios base-64); on a Node backend you can drop base-64 and use Buffer.from(...).toString("base64") instead.

import axios from "axios";
import base64 from "base-64";
const TOKEN_ID = "your_token_id";
const SECRET_KEY = "your_secret_key";
const API_BASE_URL = "https://api.fastpix.com/v1/on-demand";
export async function generateSignedUrl(metadata = { uploadedBy: "react_native_app" }) {
const auth = `Basic ${base64.encode(`${TOKEN_ID}:${SECRET_KEY}`)}`;
const body = {
corsOrigin: "*",
pushMediaSettings: {
metadata,
accessPolicy: "public",
maxResolution: "1080p",
},
};
const response = await axios.post(`${API_BASE_URL}/upload`, body, {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: auth,
},
});
const data = response.data?.data;
if (!data?.url || !data?.uploadId) {
throw new Error("Failed to generate signed URL");
}
// { url, uploadId } — pass `url` to FastPixUpload; keep `uploadId` for tracking
return { url: data.url, uploadId: data.uploadId };
}

Upload a file

Create a FastPixUpload with the signed URL and the local file URI, then call start():

import { FastPixUpload } from "@fastpix/react-native-uploads";
const upload = new FastPixUpload({
endpoint: "https://storage.googleapis.com/...your-signed-url...", // the signed URL
fileUri: asset.uri, // file:// URI from your image or video picker
chunkSize: 5 * 1024, // 5 MB (minimum allowed)
});
await upload.start();

Because endpoint also accepts an async factory (() => Promise<string>), you can plug the signed-URL generator in directly. The factory runs once when start() is called, so the URL is created fresh and your token stays on your backend:

import { FastPixUpload } from "@fastpix/react-native-uploads";
import { generateSignedUrl } from "./services/SignedUrlService";
export async function uploadVideo(fileUri) {
const upload = new FastPixUpload({
endpoint: async () => (await generateSignedUrl()).url,
fileUri,
chunkSize: 16 * 1024, // 16 MB chunks
maxRetries: 3,
retryDelay: 2000,
maxFileSize: 2 * 1024 * 1024, // 2 GB limit (in KB); 0 = no limit
enableLogs: __DEV__,
});
upload.on("progress", ({ percentage }) => console.log(`${percentage}%`));
upload.on("success", () => console.log("Upload complete"));
upload.on("error", ({ message }) => console.error(message));
await upload.start();
return upload; // keep the ref to pause(), resume(), or abort()
}

Configuration parameters

The FastPixUpload constructor accepts these options:

NameTypeRequiredDescription
endpointstring or () => Promise<string>RequiredThe signed FastPix upload URL, or an async factory that returns one. The factory runs once at start(), which is useful when tokens are short-lived.
fileUristringRequiredLocal file path from your file picker. Accepts file:// URIs and plain filesystem paths.
chunkSizenumber (in KB)OptionalSize of each chunk in kilobytes. Default is 5120 KB (5 MB). Minimum 5120 KB (5 MB), maximum 512000 KB (500 MB). Must be a multiple of 256, so any N * 1024 value is safe.
maxRetriesnumberOptionalRetry attempts per failed chunk before the upload fails. Default is 5.
retryDelaynumber (in ms)OptionalDelay before the first retry. Each later retry doubles the delay (exponential back-off). Default is 1000.
maxFileSizenumber (in KB)OptionalMaximum allowed file size. 0 means no limit. Larger files fail before any network request. Default is 0.
enableLogsbooleanOptionalEnable SDK debug logging to the console. Use in development, disable in production. Default is false.

Pause, resume, and network recovery

Every chunk that finishes is confirmed by the server, so a paused or interrupted upload always continues from the last confirmed byte. Completed chunks are never re-sent.

An upload can pause in two ways:

TriggerHow it happensHow it resumes
UserYou call upload.pause()You call await upload.resume()
NetworkConnectivity is lost, or the connection switches between Wi-Fi and cellularThe SDK resumes automatically when connectivity returns

Both cases emit a pause event with a reason ('user' or 'network'), so your UI can respond:

upload.on("progress", ({ percentage }) => console.log(`${percentage}%`));
upload.on("pause", ({ reason }) => console.log(`Paused (${reason})`));
upload.on("resume", ({ fromOffset }) => console.log(`Resumed from byte ${fromOffset}`));
upload.on("success", () => console.log("Upload complete"));
await upload.start();
// Later, from a button:
upload.pause(); // pauses immediately, keeping progress
await upload.resume(); // re-syncs the server offset, then continues

An upload you paused (reason: 'user') is not auto-resumed on reconnect. That stays under your control, so a user-paused upload never restarts on its own.


Chunk-level retry tracking

Retries are tracked per chunk, not with one global counter. Each chunk gets its own budget of maxRetries attempts with exponential back-off, so one flaky chunk cannot use up the retry allowance of the others. On a network blip, only the in-flight chunk retries, and completed chunks are never re-uploaded.

Watch it live through the chunk events:

upload.on("chunkAttempt", ({ chunkIndex, attemptNumber, totalChunkNumbers }) => {
console.log(`Chunk ${chunkIndex}/${totalChunkNumbers} — attempt ${attemptNumber}`);
});
upload.on("chunkAttemptFailure", ({ chunkIndex, attemptNumber, error }) => {
console.warn(`Chunk ${chunkIndex} failed (attempt ${attemptNumber}): ${error.message}`);
});
upload.on("chunkSuccess", ({ chunkIndex }) => {
console.log(`Chunk ${chunkIndex} uploaded`);
});

Lifecycle events

Subscribe with upload.on(event, handler). Each subscription returns an unsubscribe function, which makes cleanup in React’s useEffect straightforward.

EventDescription
startedThe upload starts.
progressReports progress, uploaded bytes, and total bytes.
stateChangeThe upload state changes (from, to).
chunkAttemptA chunk upload is attempted.
chunkAttemptFailureA chunk attempt fails and will be retried.
chunkSuccessA chunk uploads successfully.
successThe upload completes.
errorThe upload fails with a non-recoverable error.
pauseThe upload is paused (carries reason).
resumeThe upload resumes (carries fromOffset).
abortThe upload is cancelled.
offlineNetwork connectivity is lost.
onlineNetwork connectivity is restored.

Control methods

MethodSignatureDescription
start()() => Promise<void>Starts the upload. Valid only from the IDLE state.
pause()() => voidPauses an active upload, keeping the last confirmed offset. Valid only from UPLOADING.
resume()() => Promise<void>Resumes a paused upload. Re-syncs the server offset first. Valid only from PAUSED.
abort()() => voidCancels the upload and releases native resources. Emits abort, then removes all listeners.
on(event, handler)(event, handler) => () => voidSubscribes to a lifecycle event. Returns an unsubscribe function.
off(event, handler)(event, handler) => voidRemoves a previously registered listener.

The SDK also exposes state, progress, and stateHistory getters. The upload moves through these states:

type UploadState =
| "IDLE" // constructed, not started
| "STARTED" // start() called, preparing
| "UPLOADING" // actively transferring chunks
| "PAUSED" // paused (by user or network)
| "RESUMED" // transitioning back into UPLOADING
| "FAILED" // stopped with a non-recoverable error
| "COMPLETED"; // all chunks uploaded

Full example: progress bar with pause, resume, and abort

A complete component wiring the lifecycle to UI controls:

import React, { useEffect, useRef, useState } from "react";
import { View, Text, Button, ActivityIndicator } from "react-native";
import { FastPixUpload } from "@fastpix/react-native-uploads";
import { generateSignedUrl } from "./services/SignedUrlService";
export function VideoUploader({ fileUri }) {
const uploadRef = useRef(null);
const [percentage, setPercentage] = useState(0);
const [state, setState] = useState("IDLE");
useEffect(() => {
const upload = new FastPixUpload({
endpoint: async () => (await generateSignedUrl()).url, // created lazily at start()
fileUri,
chunkSize: 16 * 1024, // 16 MB chunks
maxRetries: 3,
retryDelay: 2000,
enableLogs: __DEV__,
});
uploadRef.current = upload;
// on() returns an unsubscribe function — collect and clean up on unmount.
const off = [
upload.on("progress", ({ percentage }) => setPercentage(percentage)),
upload.on("stateChange", ({ to }) => setState(to)),
upload.on("pause", ({ reason }) =>
console.log(reason === "network" ? "Paused — waiting for network" : "Paused by user"),
),
upload.on("resume", ({ fromOffset }) => console.log(`Resumed from ${fromOffset}`)),
upload.on("success", () => console.log("Upload complete")),
upload.on("error", ({ message }) => console.error(message)),
];
upload.start();
return () => {
off.forEach((unsubscribe) => unsubscribe());
upload.abort(); // release native resources if the screen unmounts mid-upload
};
}, [fileUri]);
const isUploading = state === "UPLOADING" || state === "RESUMED";
const isPaused = state === "PAUSED";
return (
<View style={{ padding: 16, gap: 12 }}>
<Text>{state} — {percentage}%</Text>
{isUploading && <ActivityIndicator />}
<Button title="Pause" onPress={() => uploadRef.current?.pause()} disabled={!isUploading} />
<Button title="Resume" onPress={() => uploadRef.current?.resume()} disabled={!isPaused} />
<Button title="Abort" onPress={() => uploadRef.current?.abort()} disabled={state === "IDLE"} />
</View>
);
}

Troubleshooting

SymptomLikely causeFix
File is empty or could not be readfileUri points to a missing file, or a content:// or asset URI the native layer cannot read.Pass a resolved file:// path or plain filesystem path. Copy picker or asset URIs to a local file first.
File size exceeds the maximum allowed sizeThe file is larger than maxFileSize (in KB).Increase maxFileSize, or set it to 0 to disable the limit.
chunkSize validation errorchunkSize is outside the allowed range, or not a multiple of 256.Use a value between 5120 KB (5 MB) and 512000 KB (500 MB) that is divisible by 256 (for example 16 * 1024).
Upload never starts, or start() is ignoredstart() was called when the upload was not in the IDLE state.Only call start() from IDLE. Use resume() to continue a paused upload.
Upload stalls after switching Wi-Fi and cellularThe in-flight socket died without an offline event.No action needed. The SDK detects the switch and resumes from the confirmed offset.
iOS build cannot find native modulesPods not installed after adding the package.Run cd ios && pod install.
No events firingListeners were attached after abort(), which removes all listeners.Re-attach listeners on a new FastPixUpload instance after an abort.

Set enableLogs: true in the constructor to see detailed SDK logs while diagnosing issues, and disable it in production.


Frequently asked questions

What is a resumable upload?

A resumable upload is a way to transfer a file that lets you pause, or recover from a network drop or interruption, without starting over from the beginning. The file is sent in sequential chunks, the server tracks how many bytes it has confirmed, and the upload continues from that last confirmed offset. It is the same pattern used by standards like the open-source tus protocol, and it is what makes large uploads reliable on mobile networks.

Does the upload keep going if the app is backgrounded or closed?

While the app is running, the SDK recovers on its own: it pauses when connectivity drops or the connection switches between Wi-Fi and cellular, then resumes from the last confirmed byte when the network returns. It does not perform OS-level background uploading after the app is suspended or terminated, so keep the upload screen in the foreground until it finishes.

What is a signed upload URL?

A signed upload URL is a short-lived, pre-authorized URL that lets a device upload directly to FastPix without carrying your Access Token or Secret Key. You create it on your backend with the Direct Upload API and return only the URL to the app, so your credentials never ship in the app bundle.

Does the SDK work in Expo Go?

No. The SDK depends on native modules, which Expo Go does not include, so you get a “native module that doesn’t exist” error. Use an Expo development build (npx expo run:ios or npx expo run:android) to run uploads on a device.

What chunk size and file size can I use?

Chunk size must be between 5 MB and 500 MB and a multiple of 256 KB, so any N * 1024 value works. The default is 5 MB. maxFileSize is optional and in KB; set it to 0 for no limit.


What’s next