Petr Chalupa4 min

Faster, More Reliable Uploads with Expo Modules

FrontendEngineeringNov 27, 2025

FrontendEngineering

/

Nov 27, 2025

Petr ChalupaFrontend Engineer

Share this article

TL;DR
Boom used Expo Modules and AWS S3 multipart upload to improve mobile video uploads in its React Native app. Uploads are now 20% faster and resilient to interruptions thanks to background tasks that keep them running even when the app isn’t active. Learn how Boom built a faster, more reliable mobile video upload pipeline using Expo Modules for native background uploads and AWS S3 multipart support.

The Product: Boom

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 is not a side feature; it is how creators enter contests. That's why a reliable and smooth upload flow is critical to the experience.

The Challenge: Large Video Uploads on Mobile

Our initial implementation for mobile video uploads used a straightforward JavaScript uploader. It worked for our MVP,  but did not scale beyond it. It had several critical flaws:

  • Lack of Background Execution: JavaScript execution is tied to the app's lifecycle. If the app was backgrounded, the upload would be suspended and often terminated by the OS.
  • Handling Network Interruptions: A single network blip could cause the entire upload to fail, forcing the process to restart.

The Goal: Fast and Reliable Mobile Video Uploads

Our goal was to create an upload experience that was:

  • Fast: Transfer videos from a user's device to 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. This eliminates failed uploads and the need to restart.

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 simplified React Native integration example. 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 for mobile video uploads 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 teams looking to improve native mobile performance, Expo Modules provide an effective way to combine JavaScript flexibility with native capability.

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.

Share this article


Sign up to our newsletter

Monthly updates, real stuff, our views. No BS.