::Users·React Native·Development

Faster, more reliable video uploads with Expo Modules

Learn how Boom built a faster, more reliable mobile video upload pipeline using Expo Modules for native background uploads and AWS S3 multipart support.

Petr Chalupa

Petr Chalupa

Guest Author

Faster, more reliable video uploads with Expo Modules

This is a guest post from Petr Chalupa - a software engineer at SRTV and the organizer of React Prague.
...
Boom (App Store, Google Play) is a video-first competition platform. Creators submit clips to monthly contests across 11 categories, and users vote for their favorites. A jury then selects a winner in each category. Each winner receives a substantial cash prize.

At its core, Boom is about competitions, not just social sharing. Uploading a video isn’t a side feature, it’s the way creators enter contests. That’s why a reliable, frustration-free upload flow is critical to the experience.

The challenge: large video uploads on mobile

Our initial implementation was a straightforward JavaScript uploader. It worked for our MVP, but didn’t scale beyond it. It had several critical flaws:

  • Lack of background execution: JavaScript execution is tied to the app's lifecycle. If the app is backgrounded, the upload would be suspended by the OS.
  • Handling network interruptions: A single network blip could cause the entire upload to fail, forcing the user to start over.

The goal: fast and reliable mobile video uploads

Our goal was to create an upload experience that was:

  • Fast: Get the video from the user's phone to our servers as quickly as possible.
  • Reliable: Uploads should survive app switching, network hiccups, and other interruptions.

The solution: native background uploads with Expo Modules

We built a brand-new upload pipeline using Expo Modules. With a SharedObject, we created a long-lived, stateful upload task. By switching to AWS S3 multipart upload, large videos now upload faster and more reliably.

Here's the TypeScript interface for our UploadTask:

export type UploadTaskEvents = {
onProgress: (params: { progress: number }) => void
}
export declare class UploadTask extends SharedObject<UploadTaskEvents> {
constructor(clipPath: string, coverPath: string)
readonly parts: URL[]
clipUrls?: string[]
completionUrl?: string
coverUrl?: string
preProcess(): void
upload(): Promise<void>
}

The iOS native module that exposes the UploadTask class to the JavaScript side.

import ExpoModulesCore
public class BackgroundUploadModule: Module {
public func definition() -> ModuleDefinition {
Name("BackgroundUpload")
Class(UploadTask.self) {
Constructor { (clip: URL, cover: URL) -> UploadTask in
return UploadTask(clip: clip, cover: cover)
}
Property("parts") { uploadTask in
uploadTask.clip.parts
}
Property("clipUrls") { uploadTask in
uploadTask.clip.uploadUrls
}
.set { (uploadTask: UploadTask, uploadUrls: [URL]) in
uploadTask.clip.uploadUrls = uploadUrls
}
Property("completionUrl") { uploadTask in
uploadTask.clip.completionUrl
}
.set { (uploadTask: UploadTask, completionUrl: URL) in
uploadTask.clip.completionUrl = completionUrl
}
Property("coverUrl") { uploadTask in
uploadTask.cover.uploadUrl
}
.set { (uploadTask: UploadTask, uploadUrl: URL) in
uploadTask.cover.uploadUrl = uploadUrl
}
Function("preProcess") { uploadTask in
try uploadTask.preProcessAssets()
}
AsyncFunction("upload") { uploadTask in
try await uploadTask.upload()
}
}
}
}

React Native integration example

Below is a condensed version of the integration in React Native. The native module handles chunking, concurrency, retries, and background execution.

// instantiate an UploadTask that owns the parts, progress, and URLs
const uploadTask = new BackgroundUpload.UploadTask(videoUri, thumbnailUri)
// listen for progress
const progressListener = uploadTask.addListener('onProgress', ({ progress }) => {
updateProgress({ uploadProgress: progress })
})
// split the file into independent parts
uploadTask.preProcess()
// request the backend for presigned URLs
const { data: uploadInfo } = await createUploadUrls({
variables: { input: { partsCount: uploadTask.parts.length } },
})
// configure task
uploadTask.clipUrls = uploadInfo.clip.uploadUrls.map(({ uploadUrl }) => uploadUrl)
uploadTask.completionUrl = uploadInfo.clip.completionUrl
uploadTask.coverUrl = uploadInfo.cover.uploadUrl
// begin upload parts in parallel with retries
try {
await uploadTask.upload()
} catch(error) {
logError(error)
} finally {
progressListener.remove()
}

Results: faster, smaller, more reliable uploads

  • Speed: Median end-to-end upload time improved by ~20% for 100-300MB clips.
  • Reliability: No “stuck” uploads observed in our latest test runs.

Measurement: end‑to‑end = tap Agree and Post → backend confirms completion.

Conclusion: why Expo Modules are the best way to add native uploads

For high-performance, resilient features such as large media uploads. A hybrid approach that combines JavaScript and native code is often the best solution. Expo Modules is a great fit for building these kinds of features.

Compared to TurboModules, they're easier to maintain, require less boilerplate, and integrate smoothly with Expo projects. This lowers the long-term maintenance cost while giving us native performance where it matters. The investments delivered a smoother user experience and a more robust, reliable app.

React Native
Expo Modules

Share article