---
title: Reading Large Files in Capacitor Without base64
description: Why a Capacitor app runs out of memory when it reads a large file with readFile, and how to read the file as a Blob without base64 on Android, iOS and the web.
date:
  created: 2026-10-30
  updated: 2026-10-30
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor File Manager: sdks/capacitor/file-manager.md
faq: true
---

# Reading Large Files in Capacitor Without base64

Reading a large file in a Capacitor app fails in a predictable way: the app asks the Filesystem plugin for the file, the plugin answers with one base64 string, and on a video or a map archive of a few hundred megabytes that string is what runs the WebView out of memory. This guide explains where the memory goes, compares the ways to read a file in Capacitor, and shows how to get the file as a `Blob` with the [Capacitor File Manager plugin](../../sdks/capacitor/file-manager.md) and use it without ever decoding it. Its [`readFileAsBlob(...)`](../../sdks/capacitor/file-manager.md#readfileasblob) method streams the file with no base64 step in between. The plugin is part of [Capawesome Insiders](../../insiders/index.md); the free path with `fetch` and `convertFileSrc` is covered as well.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Ship a fix in one command with Capawesome Cloud Live Updates, no store review" src="https://capawesome.io/assets/banners/cloud-ship-a-fix-in-one-command.png" />
  </a>
</div>

**Key takeaways:**

- `readFile(...)` of `@capacitor/filesystem` returns a string on Android and iOS, so a 100 MB file crosses the bridge as about 133 MB of base64 and sits in the WebView heap before your code sees a byte.
- Capacitor issue #984, "Support blob/large data", was closed as not planned in January 2024 with the note that plugins should handle it.
- `readFileAsBlob(...)` of the Capacitor File Manager plugin returns a `Blob` through the WebView's local file server on Android and iOS and from the Origin Private File System on the web, with no base64 step and no bridge message.
- A `Blob` costs no heap until you decode it; hand it to `URL.createObjectURL`, `FormData` or `blob.stream()`, and reserve `arrayBuffer()` for files that fit in memory.
- Use `readFile(...)` with `offset` and `length` for a header or a byte range, never in a loop over a whole file.

## Where the memory goes

The Capacitor bridge carries plugin results as JSON messages, so a plugin that returns file contents has to return them as a string. For [`readFile(...)`](https://capacitorjs.com/docs/apis/filesystem#readfile){:target="_blank"} of `@capacitor/filesystem` that string is base64 unless you ask for UTF-8, and the documentation states that on native "the data is returned as a string" while a `Blob` is "only available on Web". A 100 MB file therefore becomes a 133 MB base64 string (base64 encodes three bytes as four characters), held in native memory, serialized into the bridge message, parsed into a JavaScript string, and finally decoded by your code into a 100 MB `Uint8Array`. Several copies of the file exist at once, and none of them is the file you wanted.

Android enforces a heap limit per app, and the crash looks like the one quoted in [Capacitor File Handling: The Complete Guide](./capacitor-file-handling-guide.md): an `OutOfMemoryError` with a growth limit of 268435456 bytes, which is 256 MB. The Filesystem documentation's answer is that working with large files "may require you to add" `android:largeHeap="true"` to the manifest, which raises the ceiling without removing it. On iOS, WebKit runs the page in a separate web content process with its own memory limit, and when the page exceeds it, iOS terminates that process; the symptom is a blank WebView rather than a stack trace.

The problem is as old as Capacitor. Issue [#984](https://github.com/ionic-team/capacitor/issues/984){:target="_blank"}, opened in November 2018 to "investigate passing blobs from native to JS and vice versa", collected reports of crashing WebViews for five years before it was closed as not planned in January 2024 with the comment that "this should be handled by plugins". A forum thread from 2021 about 300 MB KML files reached the same conclusion, and the workaround given there is the one this guide builds on.

## Four ways to read

Capacitor apps have four ways to get a file's contents into JavaScript, and they differ in what crosses the bridge:

| Method | Package | What crosses the bridge | Memory | Platforms |
| --- | --- | --- | --- | --- |
| `readFile(...)` | `@capacitor/filesystem` | Whole file as base64 or UTF-8 string | Whole file plus the encoding overhead | Android, iOS, web |
| `readFileInChunks(...)` | `@capacitor/filesystem` 7.1.0+ | One base64 string per chunk | One chunk at a time | Android, iOS |
| `fetch(Capacitor.convertFileSrc(path))` | `@capacitor/core` | Nothing; served by the WebView's local server | A `Blob`, disk-backed | Android, iOS |
| `readFileAsBlob(...)` | Capacitor File Manager | Nothing on native; Origin Private File System on web | A `Blob`, disk-backed | Android, iOS, web |

`readFileInChunks(...)` arrived in Filesystem 7.1.0 and passes the file to a callback in pieces of `chunkSize` bytes, with an empty chunk marking the end. Each piece is still a base64 string, and if you concatenate the pieces to get the whole file, you are back at a whole-file string with extra steps. It suits a parser that consumes each chunk and discards it, and it has no web implementation.

The `fetch` path and `readFileAsBlob(...)` share a mechanism. Capacitor serves the app's files through a local server inside the WebView, and `Capacitor.convertFileSrc(...)` turns a `file://` path into a URL on that server. Fetching that URL streams the bytes from disk into the WebView's blob storage without a bridge message and without base64. Chromium's [blob storage](https://chromium.googlesource.com/chromium/src/+/HEAD/storage/browser/blob/README.md){:target="_blank"} pages blobs to disk when its in-memory space fills up, which is why a `Blob` of a few hundred megabytes does not count against the heap the way a string does. The Capacitor File Manager plugin does the same two steps on native when you call `readFileAsBlob(...)`, and on the web it reads from the Origin Private File System, where the plugin keeps its files.

## Read as a Blob

To read a large file as a `Blob` in Capacitor, build the file's URI with [`getUri(...)`](../../sdks/capacitor/file-manager.md#geturi) and pass it to `readFileAsBlob(...)`:

```typescript
import { Directory, FileManager } from '@capawesome-team/capacitor-file-manager';

const readVideo = async (): Promise<Blob> => {
  const { uri } = await FileManager.getUri({
    path: 'downloads/lesson-12.mp4',
    directory: Directory.Data,
  });
  const { blob } = await FileManager.readFileAsBlob({ uri });
  return blob;
};
```

The method rejects with `FILE_NOT_FOUND` when the file does not exist and with `READ_FAILED` for any other failure, so the error handling is a branch on `error.code` rather than a match on a message. Any URI the plugin hands you works as input, including the `uri` of an entry from [`readDirectory(...)`](../../sdks/capacitor/file-manager.md#readdirectory) and the URI returned by a copy or move; the documentation states that the URIs of a persisted directory and its contents are accepted by every method of the plugin, so a file in a folder the user picked reads the same way.

If your files live in the app's sandbox and you do not need the web, you can do the same without the plugin. Call `Capacitor.convertFileSrc(path)` on the `file://` path and fetch the result, as the file handling guide shows with a camera photo. The plugin adds the named error codes, one call that also works on the web, and the URI model it shares with its other methods, so a byte range, a checksum and a `Blob` of the same file use the same `uri`.

## Use the Blob

A `Blob` is a handle to bytes, not the bytes themselves, and the point of this guide is to pass that handle on instead of decoding it. Four patterns cover most apps.

To display the file, create an object URL and give it to an element:

```typescript
const showVideo = (blob: Blob, video: HTMLVideoElement) => {
  const url = URL.createObjectURL(blob);
  video.src = url;
  video.onended = () => URL.revokeObjectURL(url);
};
```

The same URL works as the `src` of an `<img>`, the `href` of a download link and the `data` of an `<object>` that renders a PDF. Revoke the URL when the element is done with it, or the blob stays referenced until the page reloads.

To upload the file, append the `Blob` to a `FormData` and send it with `fetch` or `XMLHttpRequest`; the WebView streams the body from blob storage. This stops being true once the `CapacitorHttp` plugin is enabled in `capacitor.config`, because its patched `fetch` reads `File` entries of a `FormData` into base64 before handing the request to native code. For large uploads in that setup, and for uploads that have to continue in the background, the [Capacitor File Transfer plugin](../../sdks/capacitor/file-transfer.md) uploads straight from the file path and never touches the WebView.

To parse the file, read it as a stream and process it piece by piece. A line counter for a large CSV or KML export never holds more than one chunk of text:

```typescript
const countLines = async (blob: Blob): Promise<number> => {
  const reader = blob.stream().pipeThrough(new TextDecoderStream()).getReader();
  let lines = 0;
  let lastLineTerminated = true;
  while (true) {
    const { value, done } = await reader.read();
    if (done) {
      break;
    }
    if (!value.length) {
      continue;
    }
    lines += value.split('\n').length - 1;
    lastLineTerminated = value.endsWith('\n');
  }
  return lastLineTerminated ? lines : lines + 1;
};
```

The counter keeps one boolean between chunks rather than the unfinished line, so a KML export that is one line of several hundred megabytes costs the same as one with a million lines. A parser that has to buffer the tail of a line is bounded by the longest line, which is the price to check before choosing this pattern. `blob.slice(start, end)` gives you a range of the file as another `Blob` for the same price, which is how a parser jumps to an index at the end of an archive without reading the archive.

To verify the file, do not read it at all. `crypto.subtle.digest(...)` takes an `ArrayBuffer`, so hashing a `Blob` in JavaScript means `blob.arrayBuffer()` and a full copy in the heap. [`getFileChecksum(...)`](../../sdks/capacitor/file-manager.md#getfilechecksum) computes MD5, SHA-1 and SHA-256 natively with constant memory on Android and iOS and returns a lowercase hexadecimal string to compare with the one your server sent.

`blob.arrayBuffer()` and `blob.text()` remain the right calls when the file fits comfortably in memory and a library needs the whole thing, for example a 20 MB PDF handed to a renderer. The decision is about size, not about the API: a `Blob` lets you defer the copy until you know you can afford it.

## Read a byte range

Sometimes the first bytes are all you need. A file type check reads a signature, a media library reads a header, and a resumable parser reads an index at a known offset. For those reads, [`readFile(...)`](../../sdks/capacitor/file-manager.md#readfile) of the Capacitor File Manager plugin takes `offset` and `length` and returns only that range, base64-encoded by default:

```typescript
import { FileManager } from '@capawesome-team/capacitor-file-manager';

const readHeader = async (uri: string): Promise<Uint8Array> => {
  const { data } = await FileManager.readFile({ uri, offset: 0, length: 12 });
  return Uint8Array.from(atob(data), char => char.charCodeAt(0));
};
```

Twelve bytes of base64 decode in a microsecond, and the bridge message is tiny. The one thing not to do with these options is to walk a whole file with a growing `offset` in a loop. The plugin's documentation warns that on some storage providers every call has to skip to the offset again, which makes the loop slower with every iteration, and the result would be a whole-file string anyway. For sequential processing, use `blob.stream()` on the result of `readFileAsBlob(...)` as shown above. `@capacitor/filesystem` added the same `offset` and `length` options to its `readFile(...)` in version 8.1.0, native only.

Writing has the mirror image of this problem, since [`writeFile(...)`](../../sdks/capacitor/file-manager.md#writefile) takes `data` as a string. Write large files natively instead: let the File Transfer plugin download straight to a path, and let the [Capacitor File Picker plugin](../../sdks/capacitor/file-picker.md) return a path rather than data. [`copyFile(...)`](../../sdks/capacitor/file-manager.md#copyfile) copies with constant memory and moves the result where it belongs. The `position` option of `writeFile(...)` and [`appendFile(...)`](../../sdks/capacitor/file-manager.md#appendfile) cover the small, targeted writes.

## Where readFile is fine

`readFile(...)` is the right call for a file your code is going to hold as a string anyway. A JSON settings file, a cached API response, a text template or a small SVG read with `Encoding.Utf8` produces exactly the string you want, and the base64 detour disappears. Those files are kilobytes, and the overhead does not matter. The rule of thumb is the heap: if the file could plausibly reach tens of megabytes, the data is binary, or the file comes from the user, read it as a `Blob`.

## FAQ

### Why does my Capacitor app crash with out of memory when reading a large file?

Because `readFile(...)` of `@capacitor/filesystem` returns the whole file as a string on Android and iOS. The file is base64-encoded natively, serialized across the bridge, parsed into a JavaScript string and decoded again in your code, so several copies of a file that is already a third larger than its size on disk exist at once. Android apps hit the heap limit with an `OutOfMemoryError`, and iOS reloads the WebView. Read the file as a `Blob` instead, with `readFileAsBlob(...)` of the [Capacitor File Manager plugin](../../sdks/capacitor/file-manager.md) or with `fetch` and `convertFileSrc`.

### Does readFileInChunks fix the out-of-memory error?

Partly. `readFileInChunks(...)`, available in `@capacitor/filesystem` since 7.1.0 on Android and iOS, delivers the file as a series of base64 strings, so only one chunk is in memory at a time. It helps when a parser consumes each chunk and drops it. It does not help when you need the whole file as one value, and it has no web implementation.

### Can I read a file as a Blob without the Capacitor File Manager plugin?

Yes, for files in the app's sandbox on Android and iOS: `fetch(Capacitor.convertFileSrc(path))` followed by `response.blob()` uses the same local file server that `readFileAsBlob(...)` uses on native. The plugin adds named error codes, a web implementation on the Origin Private File System, and a URI model that also covers folders the user picked.

### Does readFileAsBlob work on the web?

Yes. On the web, the Capacitor File Manager plugin stores files in the Origin Private File System, and `readFileAsBlob(...)` reads them from there. The `opfs://` URIs are internal to the plugin, so a `Blob` is also the way to hand a file to other web APIs on that platform.

### How do I check a large file's integrity without reading it?

Call `getFileChecksum(...)` of the Capacitor File Manager plugin with `ChecksumAlgorithm.Sha256`, or MD5 or SHA-1 if your server uses those. The plugin hashes the file natively in a streaming manner with constant memory usage on Android and iOS, so the file never enters the WebView.

### Is the Capacitor File Manager plugin free?

No. It is part of the [Capawesome Insiders](../../insiders/index.md) subscription and installs from the Capawesome npm registry with your license key. The `fetch` path with `convertFileSrc` needs no plugin beyond `@capacitor/core`.

## Related posts

- [Capacitor File Handling: The Complete Guide](./capacitor-file-handling-guide.md)
- [Announcing the Capacitor File Manager Plugin](./announcing-the-capacitor-file-manager-plugin.md)
- [Persistent Folder Access in Capacitor Apps](./capacitor-persistent-folder-access.md)
- [Android Scoped Storage in Capacitor Apps, Explained](./android-scoped-storage-in-capacitor-apps.md)

## Try Capawesome Cloud

Memory limits only show on real devices, and the File Manager plugin ships in a native build. Capawesome Cloud builds and signs your app for Android and iOS without a Mac, so you can test a 500 MB file on a low-end phone minutes after the change. [How to Sign & Build Capacitor Apps in the Cloud](./how-to-sign-and-build-your-capacitor-app-in-the-cloud.md) shows the steps.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Treat `readFile(...)` as a call for strings you want and `readFileAsBlob(...)` as the call for everything else, and let the `Blob` decide when a copy happens. Before you add `android:largeHeap` to the manifest, check whether the file ever needs to be decoded at all; a video, a PDF and an upload never do, and a checksum belongs in native code. For the wider picture of files in a Capacitor app, from picking to sharing and uploading, read [Capacitor File Handling: The Complete Guide](./capacitor-file-handling-guide.md).

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
