Faster, more reliable video uploads with Expo Modules
Users•React Native•Development••3 minutes read
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.

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?: stringcoverUrl?: stringpreProcess(): voidupload(): Promise<void>}
The iOS native module that exposes the UploadTask class to the JavaScript side.
import ExpoModulesCorepublic class BackgroundUploadModule: Module {public func definition() -> ModuleDefinition {Name("BackgroundUpload")Class(UploadTask.self) {Constructor { (clip: URL, cover: URL) -> UploadTask inreturn UploadTask(clip: clip, cover: cover)}Property("parts") { uploadTask inuploadTask.clip.parts}Property("clipUrls") { uploadTask inuploadTask.clip.uploadUrls}.set { (uploadTask: UploadTask, uploadUrls: [URL]) inuploadTask.clip.uploadUrls = uploadUrls}Property("completionUrl") { uploadTask inuploadTask.clip.completionUrl}.set { (uploadTask: UploadTask, completionUrl: URL) inuploadTask.clip.completionUrl = completionUrl}Property("coverUrl") { uploadTask inuploadTask.cover.uploadUrl}.set { (uploadTask: UploadTask, uploadUrl: URL) inuploadTask.cover.uploadUrl = uploadUrl}Function("preProcess") { uploadTask intry uploadTask.preProcessAssets()}AsyncFunction("upload") { uploadTask intry 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 URLsconst uploadTask = new BackgroundUpload.UploadTask(videoUri, thumbnailUri)// listen for progressconst progressListener = uploadTask.addListener('onProgress', ({ progress }) => {updateProgress({ uploadProgress: progress })})// split the file into independent partsuploadTask.preProcess()// request the backend for presigned URLsconst { data: uploadInfo } = await createUploadUrls({variables: { input: { partsCount: uploadTask.parts.length } },})// configure taskuploadTask.clipUrls = uploadInfo.clip.uploadUrls.map(({ uploadUrl }) => uploadUrl)uploadTask.completionUrl = uploadInfo.clip.completionUrluploadTask.coverUrl = uploadInfo.cover.uploadUrl// begin upload parts in parallel with retriestry {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.




