---
title: Faster, more reliable video uploads with Expo Modules
authors: Petr Chalupa
published: October 28, 2025
categories: Users, React Native, Development
tags: React Native, Expo Modules
---

_This is a guest post from Petr Chalupa - a software engineer at [SRTV](https://www.strv.com/) and the organizer of [React Prague](https://reactprague.com/)._  
...  
Boom ([App Store](https://apps.apple.com/cz/app/we-are-boom/id6741806674), [Google Play](https://play.google.com/store/apps/details?id=com.weareboom.app)) 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.

![large video uploads on mobile](https://cdn.sanity.io/images/9r24npb8/production/61218d8d2c63e1627c0f7bdff32266f192ec90f2-1348x880.png)

## 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](https://docs.expo.dev/modules/overview/). With a [SharedObject](https://expo.dev/blog/the-real-world-impact-of-shared-objects), we created a long-lived, stateful upload task. By switching to [AWS S3 multipart upload](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html), large videos now upload faster and more reliably.

![native background uploads with Expo Modules](https://cdn.sanity.io/images/9r24npb8/production/84d2e41da16694abe96a773f8bb61f76160136f6-1374x1032.png)

Here's the TypeScript interface for our _UploadTask:_

```typescript
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.

```swift
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.

```typescript
// 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.