Faster, more reliable video uploads with Expo Modules

UsersReact NativeDevelopment3 minutes read

Petr Chalupa

Petr Chalupa

Guest Author

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

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:

Code
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.

Code
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.

Code
// 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

React Native CI/CD for Android, iOS, and Web

Learn more